唯's Blog

笔者是一个热爱编程的 Java 程序员。

0%

架构的优化就是一层加一层

当耦合性太高的时候,就加一层,作为缓冲。 当合并有单点的时候,就分开。当分开有不能统一的时候,就合并。

单体应用转向微服务本质

单一职责,关注点分离,业务解耦,服务隔离,技术异构,简化部署,易于伸缩扩容

本质:其演变最重要的驱动力时方便某个服务顺利的“死去”与“重生”。面向失败设计。

方便某个服务的 死去,重生。对应的快速发布、服务淘汰、服务自动故障恢复。

微服务治理组件

强端点若管道(SOA -> 微服务)

API 设计评审清单

  • URI 命名是否通过聚合根和实体统一
  • URI 命名是否采用名词复数和连接线
  • URI 命名是否都是单词小写
  • URI 是否暴露了不必要的信息,例如/cgi-bin
  • URI 规则是否统一
  • 资源提供的能力是否彼此独立
  • URI 是否存在需要编码的字符
  • 请求和返回的参数是否不多不少
  • 资源的 ID 参数是否通过 PATH 参数传递
  • 认证和授权信息是否暴露到 query 参数中
  • 参数是否使用奇怪的缩写
  • 参数和响应数据中的字段命名统一
  • 是否存在无意义的对象包装 例如{"data":{}'}
  • 出错时是否破坏约定的数据结构
  • 是否使用合适的状态码
  • 是否使用合适的媒体类型
  • 响应数据的单复是否和数据内容一致
  • 响应头中是否有缓存信息
  • 是否进行了版本管理
  • 版本信息是否作为 URI 的前缀存在
  • 是否提供 API 服务期限
  • 是否提供了 API 返回所有 API 的索引
  • 是否进行了认证和授权
  • 是否采用 HTTPS
  • 是否检查了非法参数
  • 是否增加安全性的头部
  • 是否有限流策略
  • 是否支持 CORS
  • 响应中的时间格式是否采用ISO 8601标准
  • 是否存在越权访问

本文转载自 ThoughtWorks 洞见。

原文链接

API设计的几条原则

平常开发中的一些代码设计感悟

代码设计原则 SOLID ,迪米特原则(Law of Demeter)

其中迪米特原则又叫最少知识原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息。还有一个更简单的定义:只与直接朋友通信。

在平时开发中,在类,方法的设计上也尽量保持这个原则。A依赖B对象,A不直接调用B对象中依赖的对象,但完全参考这个原则开发,会导致类内部方法的膨胀。因此在设计需要自己做出权衡,但是其中的平衡点并不好找。后面读了《代码简洁之道》第六章中也有提到迪米特原则,说明了某些场景并不需要遵守这个原则,或者说该原则不适用这种场景,例如:A类依赖B类,B类可能是个数据结构,无对象行为,那么直接调用B内部的数据是可以的。然而在 Java 中因为定义私有变量的原因(内聚、封装),提供了大量的访问方法(get)、该值方法(set)

其中还提到了 隐藏结构,接着上面那个例子,假设 A 依赖 B,B就是具有行为的对象,我们在定义 B 对外的行为,应该更加符合我们系统的业务行为。比如:

1
2
3
4
5
6
// A 拿到的B的某个值(可能在B内部经过大量计算)
int s = A.B.getxxx
s....
// 好的定义,应该定义好对象行为
A.B.craeatexxx

这样满足了 迪米特原则,也不会导致 B 对象暴露内部过多的数据结构。

好的代码

  • 通常说到好的代码,指的都是:好的可读性,好的命名,干脆的抽象,应用设计模型,选择适合的架构,兼顾 可扩展、高可用、可监控等等。
  • 我想说的是,要编写好的代码的前提一定是 开发者要用心,用心 便会思考命名、抽象、充分酌量架构的取舍,用心便会写出好的代码。

系统架构

  • 系统架构,应该区分核心功能,附加功能,前期针对核心功能定义好架构、模型、流程。核心功能的定义决定了系统的稳定性,好的定义对于系统设计也就成功一半了,额外功能、边角功能也就水到渠成了。
  • 避免过度设计,不要先做大的设计。
  • 一个好的系统,一定离不开一套好的模型定义。梳理清楚系统中的核心模型,清楚的定义每个方法的类归属,无论对于代码的可读性、可交流性,还是和产品沟通,都有着莫大的好处。

整洁代码

  • 不要重复代码,只做一件事,表达力,小规模抽象

怎么理解重构?

重构不是把整个系统推到重来,把一个功能代码重新设计也是重构,提取一个方法也是重构。

重构是研发最重要的能力,重构的目的就是让代码符合设计原则,例如:单一原则。所有的方法可以是单一职责,即使是复杂的业务方法也是单一职责,那就是分发与聚合,典型的便是策略模式。

抽象

我的理解:抽象的本质是找事务的共性,找到共性便能抽取、抽象。

之前做的导出报表的功能,Release 有 操作时间、操作人、操作类型,Confirm 也有操作时间、操作人、操作类型(approve同理),这里就能找到Release、Confirm、Approve针对报表导出这个功能的共性,进行抽象

不要盲目追求性能,所谓的流行架构 (KISS[保持简单])

之前有个项目需要导出很全面的报表、还存在汇总计算的需求。因为当时项目牵扯到多方的数据源,如果实时去查询多个数据库聚合会导致接口响应慢,同时存在数据量大的情况甚至会拉垮服务。

我们当时想了一套方案:建立一个新的数据源(MongoDB),通过 ETL 清洗一份新的数据结构存储在数据源。

这里存在数据同步实时的问题,我们使用 Job 去同步新的数据源数据则会存在不准确。正对这个问题我们又想到 Flink 分布式实时处理引擎。

仔细一想,我们需要实时吗?我们沟通了需求,报表的内容一般是只会查看上个月的项目信息。因此实时是个伪需求,不要为了用新技术,而新技术,技术是用来服务业务的。

Unix哲学

  • 程序应该只关注一件事,并把做好。

引申出来的关于程序设计的格言:

  • 过早的优化是一切罪恶的来源
  • 尽量使用简单的算法和数据结构
  • 数据决定一切,选择对的数据结构,算法就无关痛痒了

Feild 注入

@Autowired

@Resource

推荐使用 @Resource ,@Resource 支持 name type,默认 name 注入,同时 它是 JDK 的注解,减少依赖

构造方法注入

Spring 官方推荐 构造方法注入,具体的优势:

  1. 单一职责:指的是如果一个类的依赖太多,构造方法就会显得很臃肿,这时我们就得思考当前类是否承担了太多职责。假如使用 @Autowired 注入 field 可能就不会发现问题。

  2. 依赖不可变:只用使用构造方法注入才能将字段定义成 final

  3. 依赖隐藏:清晰的知道类需要什么

  4. 降低容器耦合度:使用构造方法注入他只是一个普通的类,不依赖 DI 容器。如果没有容器耦合,我们可以将该类交给容器管理也可以自己管理,同时切换 DI 框架也很方便。

Setter注入

Config 模块

  • 是否设置了使用外部存储,是则使用外部存储,否 则判断是否开启内嵌数据源存储,默认单机模式则开启内嵌数据源存储,若没有开启则自动升级为外置数据源存储,和之前一样

  • 
    // 内嵌数据源
    
    LocalDataSourceServiceImpl
    
    // 外部数据源
    
    ExternalDataSourceServiceImpl
    

Auth 模块

  • AuthFilter 实现接口的权限验证,注解 @Secued

Core 模块

  • RequestHandlerRegistry 注册定义的容器中的 RequestHandler
监控数据指标
  • NacosMeterRegistryCenter 基于 CompositeMeterRegistry (micrometer)实现

Console 模块

  • 用于管理nacos,监控,展示Namespace、服务状况

Common 模块

  • NotifyCenter 通知中心

  • Cache ,CacheBuilder 可以构建:SimpleCache 、LruCache、SynchronizedCache、AutoExpireCache

Client 模块

  • Limiter 用于限流,底层使用 RateLimiter(guava工具包)

搭建步骤

  1. 在自己的 GitHub 仓库中创建一个新的仓库,仓库命名 username.github.io,例如我:izachwei.github.io
  2. 在该仓库放入博客静态页面,可以使用 Hexo 生成博客前端界面
  3. 访问地址:username.github.io 访问成功

Tips

  • categories: - [前端] - [Hexo] 这样写支持一篇文章多个分类的

1
2
3

docker run -d --name selenoid -p 8081:4444 -e TZ=America/Los_Angeles -v /var/run/docker.sock:/var/run/docker.sock -v /opt/selenoid/config/:/etc/selenoid/:ro -v /opt/selenoid/video/:/opt/selenoid/video/ -v /opt/selenoid/logs/:/opt/selenoid/logs/ --restart always selenoid:latest -conf /etc/selenoid/browsers.json -limit 20 -video-output-dir /opt/selenoid/video/ -service-startup-timeout 120s -session-attempt-timeout 120s -session-delete-timeout 300s -timeout 1000s -log-output-dir /opt/selenoid/logs/

创建 Selenoid_UI 容器

1
2
3

docker run -d --name selenoid-ui --restart always -p 8082:8080 aerokube/selenoid-ui --selenoid-uri=http://172.16.139.29:8081

1
2
3

{ //... "port": "", "tmpfs": {"/tmp": "size=512m", "/var": "size=128m"}, "path" : "", "volumes": ["/from:/to", "/another:/one:ro"], "env" : ["TZ=Europe/Moscow", "ONE_MORE_VARIABLE=itsValue"], "hosts" : ["one.example.com:192.168.0.1", "two.example.com:192.168.0.2"], "labels" : {"component": "frontend", "project": "my-project"}, "sysctl" : {"net.ipv4.tcp_timestamps": "2", "kern.maxprocperuid": "1000"}, "shmSize" : 268435456, "mem":"1024m"}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

4.9. Selenoid CLI Flags

The following flags are supported by selenoid command:



-capture-driver-logs

Whether to add driver process logs to Selenoid output

-conf string

Browsers configuration file (default "config/browsers.json")

-container-network string

Network to be used for containers (default "default")

-cpu value

Containers cpu limit as float e.g. 0.2 or 1.0

-disable-docker

Disable docker support

-disable-privileged

Whether to disable privileged container mode

-disable-queue

Disable wait queue

-enable-file-upload

File upload support

-graceful-period duration

graceful shutdown period in time.Duration format, e.g. 300s or 500ms (default 5m0s)

-limit int

Simultaneous container runs (default 5)

-listen string

Network address to accept connections (default ":4444")

-log-conf string

Container logging configuration file

-log-output-dir string

Directory to save session log to

-max-timeout duration

Maximum valid session idle timeout in time.Duration format (default 1h0m0s)

-mem value

Containers memory limit e.g. 128m or 1g

-retry-count int

New session attempts retry count (default 1)

-save-all-logs

Whether to save all logs without considering capabilities

-service-startup-timeout duration

Service startup timeout in time.Duration format (default 30s)

-session-attempt-timeout duration

New session attempt timeout in time.Duration format (default 30s)

-session-delete-timeout duration

Session delete timeout in time.Duration format (default 30s)

-timeout duration

Session idle timeout in time.Duration format (default 1m0s)

-version

Show version and exit

-video-output-dir string

Directory to save recorded video to (default "video")

-video-recorder-image string

Image to use as video recorder (default "selenoid/video-recorder:latest-release")

创建GGR

docker run -d –name ggr -p 8088:4444 -e TZ=America/Los_Angeles -v /etc/grid-router/:/etc/grid-router/akamai/:ro aerokube/ggr:1.5.4

GGR 配置更新,不停机更新

docker kill -s HUP ggr

数据结构

  1. String:SDS

  2. 字典(dict):ziplist、hash

  3. list:ziplist、双向链表

  4. set:整数集合(int[])、hash

  5. zset:ziplist、跳表

特性

Redis 事务不支持原子性,持久性也需要 AOF 配置的支持,修改成 always

优缺点

优点

  1. 支持的数据结构丰富

  2. 基于内存操作、响应快

  3. 支持主从、分布式满足高可用

  4. 读写数据是单线程操作、避免了上下文切换 保证了原子性

  5. 支持持久化操作、RDB AOF

缺点

  1. 数据库容量受到物理内存的限制,内存资源较为珍贵,支持少量数据的读写,Redis适合热点数据、较少的数据量上的读写操作。

  2. 主机宕机,主从同步之间存在延时,会导致丢数据

  3. 在线扩容很复杂

按照上述四个方面进行方案设计:

  1. 场景功能

大文件、网络传输

大文件传输分为 数据同源 和 异构 两种方案

异构则需要考虑存储协议的定义优化 (序列化、反序列)

存储协议 -> 网络协议 -> 流式处理

考虑异常、断点续传、保证完整性

文件保存的安全性:备份?读取文件考虑效率?

  1. 服务

Client 、Server 、存储层、Manager(分片信息管理 、聚合、分布式存储等)

  1. 存储

大文件存储肯定是尽量打散、降低单节点的网络压力、每个节点内部根据数据类型管理他的内存存储结构(avro(序列化系统)、parquet(列示存储))、使用分布式文件存储系统:HDFS

  1. 流程设计

  • 客户端拆分大文件生成多个文件分片,调用服务端生成一个文件上传任务(分片相关信息)返回本次分片上传唯一标识

  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件

  • 要求点对点传输、基于 TCP 长连接流式传输,服务端 buffer -> flush ,利用 buffer 建立可监控、可控速的网络流传输。生产 消费模型。

  • 为了保证内存不溢出、使用有有界内存队列 BlockingArrayQueue ,控制网络流传输速率,写入 系统缓存 ,定时统一写入数据库。网络IO

  • 如何实现断点续传

一般实现方式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可

如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

分片写 、多线程 、切片传输(压缩数据、考虑异常、断点续传、保证完整性)、检验 SHA1、UDP 分片 分组、考虑网络I/O 磁盘I/O 瓶颈

网络的传输考虑使用 Netty 框架 NIO

异常情况的回滚 :使用数据库的事务,每个大文件的接收,创建一个大事务、每个分片写入数据库、存在问题回滚大事务

分片:

定义分片信息的数据结构

分片要考虑网络的实时传输 动态改变分片大小

为什么选择 TCP ?

TCP 面向字节流、可靠

丢包?数据传输异常中断?

SHA1 校验 文件内容完整性、重传

项目目标:零学习成本、低维护、高可用的自动化测试方案

  • 易用性:工具/平台的上手难度,使用复杂度应该尽可能的低,因为自动化测试的目的是提效人力,而不是增加人力负担。

  • 平台支持:移动端至少需要覆盖移动端和网页端双平台,同时基于外卖的业务特点,不仅需要对Native支持,也需要支持Mach(自研局部动态化框架)、H5、React Native、美团小程序等技术栈。

  • 稳定性:自动化测试用例的执行需要有足够的稳定性和准确性,测试过程中不应因测试工具本身的不稳定而出现稳定性问题。

  • 维护成本:维护成本很大程度上决定了测试工作量的大小,因需求产生变动或架构重构等问题时,用例的维护成本应该尽可能的小。

  • 可扩展性:当测试方案不能满足测试需求时,工具/平台应具备可扩展的能力。

image-20221111144436624