2022-01-23T12:28:42+00:00 ccf.developer@gmail.com 实践 DDD:值对象与实体 2022-01-23T00:00:00+00:00 http://huangtuzhi.github.com/2022/01/23/DDD-valueobject DDD 指的是 Domain-driven design(领域驱动设计),这套理论讲述的是如何在业务开发中建立业务模型,本文介绍最基本的实体和值对象概念。


从属性系统说起

以电商中的属性系统为例,搜索 iPhone 会展示很多筛选属性。机身内存 ROM、网络类型是「属性」,512G、256G、无需合约版、TD_LTE 是「属性选项」。表示为结构化的数据结构就是

{
  "rom":[512, 256, 128],
  "net_type": ["无需合约版", "TD_LTE"]
}

属性和属性系统应该如何存储到数据库中。一种方式是把属性和属性选型放在商品表的一个字段中。


商品 ID 商品名 属性
1001 iPhone12 大小:512G,颜色:白
1002 Huawei 大小:256G,像素:1 亿
1003 Xiaomi3 屏幕材质:OLED,尺寸:200 寸

如果属性和属性选项只是用来展示,这样存完全没有问题。但现在有这样的需求场景:

  1. 把商品中属性「屏幕材质」的名称修改为「屏幕材料」
  2. 用户搜索时,展示所有内存大小为 512G 的手机

第一个场景需要替换所有记录中的字段名称,第二个场景很难高效检索出来。这个时候就需要采用一种新的设计方式:

给属性新增自增标识符属性 ID,需要关联的地方使用属性 ID。


属性 ID 属性名
50001 颜色
50002 屏幕材质
50003 尺寸

给属性选项新增自增标识符属性选项 ID,需要关联的地方使用属性选项 ID。


属性选项 ID 属性选项名 属性 ID
3001 50001
3002 50001
3003 OLED 50002
3004 LED 50002
3005 512G 50003
3006 256G 50003

这样商品存储就变成了:


商品 ID 商品名 属性选项
1001 iPhone12 3005, 3001
1002 Huawei 3005, …
1003 Xiaomi3 3003, …

通过新增唯一标识符的方式,可以解决以上两个问题。当字段名修改时,由于表中关联的是 ID,所以无需改动。针对第二种场景一般使用搜索引擎,给属性选项字段建立倒排索引,这样就能实现高效检索。


值对象与实体

在 DDD 中第一种表示叫值对象,第二种叫实体。他们的区别是,实体拥有唯一标识符(主键 ID),且标识符在经过任何变化后仍然保持不变。对于这种实体,它其中的属性可以发生改变。而实体标识符的延续性会带来系统设计上的好处,以不变应万变。

]]>
LVS 工作流程 2021-06-25T00:00:00+00:00 http://huangtuzhi.github.com/2021/06/25/how-lvs-work 一个客户端请求发送到服务器,会经过域名解析获取服务器 IP,请求到达业务服务器会经过哪些网络设备?这里面的详细流程是怎么样的?


域名解析过程

域名的解析过程由本地 DNS 服务器 -> 根域名服务器 -> .com 顶级域名服务器 -> 网站权威域名服务器。

fuzhii.com 的权威域名服务器是 whois.dnspod.cn,腾讯云旗下品牌 DNSPod 是这个域名的服务提供商,域名解析时会逐级询问到这个服务器。

在 DNSPod 管理平台上查看,可以看到

NS 记录代表域名服务器记录,如果需要把子域名交给其他 DNS 服务器解析,就需要添加 NS 记录。

DNSPod 将子域名的解析交给了 pony.dnspod.net。

NS 一般用于配置全局负载均衡 GSLB。比如腾讯的一级域名 qq.com 就配置为了 NS1.QQ.COM 用作 top GSLB NS 域名。


GSLB

在域名服务商那里配置好 GSLB NS 后所有的域名解析都会交给 GSLB 来处理。GSLB 主要功能是可根据访问者的位置,提供就近接入能力,减少请求耗时。GSLB 相当于公司/个人定制的 DNS 域名解析器。

在 GSLB 上给域名(app.com.cn)绑定服务器对应的公网 IP。这些映射表由运维负责维护和配置。

app.com.cn 14.215.140.116
app.com.cn 183.3.235.18

这些 IP 是内部服务器的真实 IP 吗?内部服务器由于安全原因不会配置外网策略,只有内网 IP。那如何让外网用户访问到部署在内网的服务?


LVS

查看 Nginx 上的网络配置

Nginx1=14.215.140.116;183.3.235.18;

Nginx2=14.215.140.116;183.3.235.18;

多台 Nginx 上都配置了相同的 IP,这是怎么做到的?IP 不会冲突吗?这样做的好处是什么?

DNS 过程:客户端 -> GSLB -> 返回 app.com.cn 域名配置的一个 IP 14.215.140.116

TCP 连接过程:client -> LVS LD(Load banlance Director)-> RS(Real Server,这里对应 Nginx)

LVS LD 和 RS 网卡上配置的都是 14.215.140.116,LD 会通过 IP 隧道技术将请求从 LD 转发到 RS。


IP 隧道技术

使用 ifconfig 可以看到这个 Nginx 模块有 14.215.140.116 等公网 IP,这个公网 IP 其实是由 LVS 在这个机器上配置的虚拟 IP(VIP)。

tunl0:0: flags=193<UP,RUNNING,NOARP>  mtu 1480
        inet 14.215.140.116  netmask 255.255.255.255
        tunnel   txqueuelen 0  (IPIP Tunnel)

tunl0:8: flags=193<UP,RUNNING,NOARP>  mtu 1480
        inet 183.3.235.18  netmask 255.255.255.255
        tunnel   txqueuelen 0  (IPIP Tunnel)

LVS 使用内核模块虚拟出了一些网卡,再在这些网卡上绑定公网 IP。

整个转发流程如图所示:

这样用户就访问到了内网的服务,LVS 可以将用户请求通过策略任意转发到对等的 Nginx1 或 Nginx2 上,这样就实现了网络层的负载均衡。


参考

Linux 中 IP 隧道

Nginx 四层七层代理区别

LVS 负载均衡原理

]]>
如何获取客户端 IP 2021-06-11T00:00:00+00:00 http://huangtuzhi.github.com/2021/06/11/how-to-get-client-ip 一台手机连接到 Web 服务器进行支付下单操作,Web 服务器需要获取手机的真实 IP 来做一些业务相关的策略,比如分析用户行为、限制请求频率等。用户请求经过了交换机、路由器等网络硬件设备,Nginx 等负载均衡器,Web 服务器如何获取到客户端 IP?


为什么需要 IP 和 MAC 地址?

手机发出网络请求,请求会携带 IP 和 MAC 地址。IP 已经可以定位到手机在网络中的具体位置,为什么还需有 MAC 地址?没有 MAC 地址可以完成通信吗?

先看一下 IP 报文协议

报文中有源 IP 和目标 IP 地址,没有其他的字段来存储中转信息。

现在客户端 S 经过 A、B 路由器到达服务器 D,S 发到中转点 A 时必定要在报文中写入和 A 地址相关的信息,比如 A 的 IP 地址,这样请求到达 A 的时候 A 才知道这个请求是发给自己的。但实际上 IP 报文并没有一个中转 IP 字段。

如果可以重新设计网络协议,你可以在 IP 报文协议中加入一个中转 IP 字段,这个字段用来记录需要中转的地点。S 发往 A 时,中转 IP 填写为 A 的 IP 地址;A 发往 B 时,中转 IP 填写为 B 的 IP 地址。

而 TCP/IP 网络模型已经做了这个事情,这个中转 IP 就是 MAC,只不过它是在链路层实现的。如果协议按照中转 IP 重新设计,可以完成通信吗?如果网络只有几个节点,路由记录下转发路由表,理论上是可以通信的。但节点增多,路由就存不下了。而且在新建路由表时需要有个广播探测建表的过程,这个广播报文会发送到所有机器,会造成通信阻塞完全不可用。

怎么进行优化?分而治之?把几台机器分到一个组(局域网)里。这样路由表只用记录组里 leader 的 IP,这个 leader 就是网关。在组里通信的时候使用 MAC 来寻址,在组外就使用 IP 来寻址。MAC 寻址需要使用广播这种方式,广播更接近物理硬件实现。这个 MAC 地址能否作为中转信息放在网络层呢?为什么需要分这么多层?


为什么网络协议要分层?

分层是计算机系统中的常见操作,看看常用的互联网服务架构,从最底层到最上层依次是:

Data:数据层,包括 MySQL、Redis 等。存储用户、订单、商品数据。

DAO:Data Access Object,数据读取层,对基础数据的 CRUD。比如读取用户基本信息。

AO:逻辑组合层,对 DAO 层的数据进一步处理。比如对用户基本信息和订单信息组合,封装为一个 GetHomePage() 的接口,用于在网站首屏展示用户 home 页。

CGI:接入层,如查询用户首页,调用 GetHomePage(),展示给用户首页信息。

这样做的好处是逻辑解耦、职责分明,每一层负责特定的事务。当需要进行修改时,只需要在这一层修改而不影响其他层的服务。比如需要新增一个查询用户最近一年生活用品消费总额接口,那么在 AO 层组合查询订单 DAO 和查询商品详情 DAO 的返回数据即可。

TCP/IP 协议模型也是同样的原理。

物理层:主要是硬件电路,负责原始比特流的处理和传输。比如将模拟电\光信号转换为二进制流。代表设备是集线器。

链路层:负责 MAC 帧的处理,将源 MAC 地址、目标 MAC 地址、协议类型字段封装。代表设备是交换机。

网络层:负责 IP 的处理,将源 IP 地址、目标 IP 地址等字段封装。代表设备是路由器。

传输层:负责 TCP/UDP 处理,将源端口、目的端口等字段封装。

应用层:负责和应用相关的协议处理,比如 HTTP、FTP。

这样分层后,应用层需要可靠的通信协议,可以使用 TCP,自己不用保证可靠性;应用层也可以基于 UDP 自己实现可靠性(比如 QUIC 协议)。如果没有分层,TCP 协议耦合在了所有层,应用层就没办法定制化了。

同时,网络层可以专心处理在组(局域网)之间的通信;链路层专心处理在组(局域网)内的通信。


Nginx 代理与配置

请求经过交换机在局域网内转发,再经过路由器在局域网之间转发后到达了 Nginx。手机最开始通过域名解析获取到的服务器 IP 其实就是 Nginx。这个 TCP 的请求其实已经结束了,那么请求最终怎么到达 Web 服务器?

查看 Nginx 的配置

server {
    listen      80; # 监听80端口
    server_name localhost; # 配置域名

    location ^~ /gethomepage {
        proxy_pass       http://9.141.171.14:11648; // 内网 IP
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
    }
}

请求通过 url 匹配到规则后转给给了内网的 IP 9.141.171.14,这时 Nginx 就是新的客户端了。通过 request.getRemoteAddr 获取到的 remote_addr IP 是 TCP 底层会话 socket 连接的 IP,也就是 Nginx 的地址,显然不是客户端的 IP。获取请求到服务器的客户端 IP,可以通过读取 HTTP Header 中的字段。

  • X-Real-IP:Nginx 中可以配置,将上一级的 remote_addr 设置为 X-Real-IP
  • X-Forwarded-For:记录完整的代理链路,可以伪造

这两个字段都需要在 Nginx 中进行配置

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

这样请求从客户端经过交换机、路由器、Nginx 到达 Web 服务器,我们就获取到了真实的用户 IP。

]]>
演习与混沌工程 2021-04-16T00:00:00+00:00 http://huangtuzhi.github.com/2021/04/16/chaotic-engineering

Chaos Is A Ladder

现代战争中为了评估战力、保证军备强度会进行实战的军事演习,日常生活中也常常有消防演习、地震演习。军事演习真正发射了防空导弹,消防演习也真的点了一把火,另外我也真的有一头牛。这些是真实的行动,不是推演、假设,要的效果就是和现实世界一样。也只有这样当灾难发生,我们的应对措施才有效。


名词解释

  • BCP:Business Continuity Planning,业务持续计划。BCP 是为了防止正常业务行为被中断而建立的计划。 当面对自然或人为造成的故障引起正常业务不能使用时,BCP 可以用来保护关键业务步骤,类似于应急预案。比如消防演习时制定的撤离计划,再比如 MySQL 故障时的备机切换方案。下图是购买车险业务中,当专线出现故障时的 BCP。

  • 演习:在真实的环境中进行演练,为了保证有效性,严格来说演习必须在生产环境。演习建立在系统完成 BCP 的条件下进行。
  • 反脆弱:Antifragile,塔勒布写的一本书,讲述如何在不确定性中获益。“对随机性、不确定性和混沌也是一样:你要利用它们,而不是躲避它们。你要成为火,渴望得到风的吹拂。这总结了我对随机性和不确定性的明确态度”。我们需要让系统从每一次故障、失败中受益,不断进化。
  • 混沌工程:大学时的电路学过非线性电路混沌电路,实现了最简单的混沌系统。混沌系统的特点就是随机、不确定性。而混沌工程理论是建构于塔勒布的反脆弱思想之上。混沌工程提倡用一系列实验来真实地验证系统在各类故障场景下的表现,通过频繁地主动引发故障进行大量实验,使得系统本身的反脆弱性持续增强,也让开发者对系统越来越有信心。

混沌系统

业界对混沌系统的研究已经很多年了,一些大厂在业务中已经大量实践。混沌系统是混沌工程的系统集成,用于快速进行实验,主要包括故障注入、稳态监控等,适配的平台有物理机和 Kubernetes。


原子故障

很多业务系统故障和最基本的系统资源异常有关,比如 OOM、CPU 满载、进程退出,把这种系统资源异常叫做原子故障。主要分为下面几类

看几个原子故障的实现原理


网络延时

网络延时可以通过混沌系统 agent 下发执行 iptables DROP 命令来实现

封禁指定 IP 访问本机

iptables -I INPUT -s 192.30.252.154 -j DROP

封禁本机访问指定 IP

iptables -I OUTPUT -d 192.30.252.154 -j DROP

封禁后的 iptables 规则

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
DROP       tcp  --  anywhere             lb-192-30-252-154-iad.github.com

CPU 负载

CPU 负载是通过混沌系统 agent 新开一个进程去空跑 for 循环来消耗 CPU 时间片。

for i in `seq 1 $(cat /proc/cpuinfo |grep 'physical id' |wc -l)`; 
do dd if=/dev/zero of=/dev/null & done && sleep MINUTEm

异常事件库

在实际应用中,我们在应用层遇到的故障可能是 RPC 接口失败、kafka 队列满等。这些原子故障不能满足需求,需要在 Iaas 层、Paas 层、Saas 层基于原子故障去建立不同层次的异常事件库。


实战演习

业务中使用一个唯一 ID 生成器 IDGen,它是一个独立的进程。IDGen 的 BCP 方案是在共享内存中缓存了 2 个小时的 ID,这次演习 IDGen 故障,看业务是否受到影响。思路是使用混沌工程平台将进程异常事件注入到机器上。

进程退出后,业务正常,说明 BCP 方案生效,演习成功。

如果你渴望和平,就必须做好战争的准备 – 天启者·卡尔玛

]]>
协程切换会引起什么问题 2021-04-02T00:00:00+00:00 http://huangtuzhi.github.com/2021/04/02/coroutine-switch 在微信的业务中协程被大规模应用,在使用的过程中遇到了一些和业务场景相关的问题。本文自下而上从 CPU 中断到 Linux 内核态、再到用户态进程、最后到协程,由这种视角去分析协程切换时引起问题的原因,希望可以更好地理解协程。

计算机里有两类大的资源:CPU 资源和 IO 资源。计算型的任务主要消耗 CPU 资源,比如对字符串进行 base64 编码,我之前遇到 3W/min 的线上接口加了 base64 编码把 CPU 跑满;输入输出类的系统调用主要消耗 IO 资源,部分 IO 和硬件中断相关。CPU 芯片引脚上接入了很多控制芯片,比如中断控制器芯片 8259A。当键盘打字,中断芯片触发 CPU 上的硬件中断,CPU 被调度来处理键盘输入。

计算机上有这么多的任务需要消耗 CPU 和 IO 资源,操作系统怎么去优化资源利用呢?

对于计算密集型任务,可以用多进程/线程将任务分发到不同的 CPU 核上并行处理来提高效率;对于 IO 密集型任务,操作系统已经有中断回调机制,对于正在到来的 IO 事件进行处理。


中断机制

外围硬件设备连接到中断控制器芯片上,产生的电信号经过中断控制器芯片编码后写到 CPU 的控制寄存器中。那 CPU 怎么知道寄存器发生了改变?

当执行了一条指令后,cs 和 eip 这对寄存器包含了下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常,如果发生了一个中断或异常,那么控制单元执行下列操作 ——「深入理解 linux 内核」

也就是 CPU 在每个指令周期都会去查看中断寄存器,这是硬件级别的轮询。用这种轮询来实现了中断控制,这种中断回调存在于内核态。


操作系统之上再造系统

业务的进程中,既需要使用 CPU,又需要使用大量 IO。怎么去优化这种场景?

在进程中再开线程?在线程中出现 IO 调用时(主要讨论同步 IO,Linux 中还没有完善的异步 IO),让线程睡眠,让其他的线程去处理任务。假设现在 8 核 CPU 上有 4 个进程,每个进程开 10 个线程,理论上也只能同步并行跑 8 个线程,其他线程都是假性的并行运行。如果这时出现大量的阻塞 IO 调用,所有的线程都会进入睡眠,等待同步 IO 的数据返回;如果是非阻塞 IO 调用(O_NONBLOCK),使用 poll/epoll 来轮询事件到来,虽然不会进入睡眠,但线程不断从内核态到用户态的上下文切换效率较低。

有什么办法可以让用户态的进程/线程中拥有一种异步回调的能力,在发生 IO 调用时切换到其他进程/线程,又能保证不睡眠进入内核态。在 IO 事件到来时,再切换回这个进程/线程,整个过程都是在用户态完成。这相当于在操作系统上重新造了一个操作系统来进行进程/线程调度,梦境之上再造梦境。


协程是什么

想象一个人进入到进程中,现在有两个子函数:一个函数负责倒开水,一个函数负责晾衣服。当人看到开水烧开了,调用函数去倒开水;当看到衣服洗好了,调用函数晾衣服。这个人就是程序员本人,他来负责子程序的调度。子程序就是协程,这个人就是程序员本人,可以看作是人肉协程调度器。

ucoroutine 是一个协程调度的示例,使用 Linux getcontext、makecontext、swapcontext 函数簇来实现协程切换。

那么人肉调度器可以由什么来自动化替代呢?这个调度器需要实现这些功能

  • hook 掉同步 IO 调用函数

比如对于网络应用场景,hook 掉 socket 簇 read、write、connect、send、recv 函数,让这些函数调用时发生用户态协程切换,同时记录下相应的上下文信息

  • 加入异步回调机制,在 IO 事件到来时回调到事件对应的处理协程

这个东西是不是可以用 epoll 实现?在 IO 调用时把事件注入到 epoll 事件池,同时发生协程切换,等 IO 事件到来时由主协程用 epoll 去回调切换到对应的协程。

void ucoroutine_body(schedule_t *ps)
{
	int id = ps->running_coroutine;

	if (id != -1) {
		ucoroutine_t *t = &(ps->coroutines[id]);
		t->func(t->arg);
		// 模拟函数阻塞,进行调度。在这里将事件注入到了 epoll 中即可
		puts("before yield");
		ucoroutine_yield(*ps);
		puts("after yield");
		t->state = IDLE;
		ps->running_coroutine = -1;
	}
}

这样就实现了用户态的操作系统。

常见的协程库实现有三种方式:

  • 使用 glibc 的 ucontext 库
  • 使用汇编代码切换上下文,微信 libco 使用这种方式
  • 利用 C 语言的 setjmp 和 longjmp

这些方式的原理都是通过保存和恢复寄存器的状态,来进行各协程上下文的保存和切换。


切换引起的问题

在使用 libco 时,已 hook 掉所有的 socket read/write/send/recv 系统调用,在发生这些系统调用时,libco 会切换协程,将当前的上下文保存,切换到其他协程使用 CPU。所以在业务代码中发起 RPC 调用会触发协程切换,这种切换会引起一些问题。

问题 1:”幻读“

最近在进行一个项目的改造,有这样一段代码

m_config = config; // 进程变量
m_config->cmdid() = 100; // 这个值根据请求不同会变化

id_a = m_config->cmdid()
CallRPC()
id_b = m_config->cmdid()

这个代码段有多个协程共用,最后发现 id_a != id_b,出现了”幻读“问题。

这是因为 CallRPC 时会发生协程切换,等再次切换回来,cmdid 已经被其他协程修改了。

使用协程原则:全局变量和静态变量为所有协程共享,不要使用全局变量,静态变量。

问题 2:死锁

NoticeConfig::NoticeConfig()
{
	CallRPC();
}

NoticeConfig* NoticeConfig::GetDefault()
{
	static NoticeConfig conf; // 卡在这里
	return &conf;
}

func()
{
	auto conf = NoticeConfig::GetDefault();
}

协程 A 中 func 调用 GetDefault 获取单例,GetDefault 中初始化 conf 时,构造函数调用了 RPC 发生了协程切换,CPU 让给协程 B。而对于 static 变量,gcc 在构造时会加锁,所以 A 获得了这把锁。当协程 B 调用 func 时,conf 还没初始化完成,会尝试再次加锁。所以其他协程都会卡死在这里。

这种场景出现需要满足两个条件

  • 构造函数中加了 RPC 操作
  • 用 static 来实现单例

总结

从上面的分析可以知道协程的使用场景

  • 频繁发生 RPC 调用,可以最大程度使用协程的能力。相比线程节省系统资源
  • 非计算密集型任务,CPU 计算不复杂。CPU 计算会占用大量时间,会让协程占用 CPU 时间过长,影响其他协程正常运行。同理 sleep 也不能使用,会让进程睡眠而无法进行切换,使用 poll(NULL, 0, sec) 代替。

另外操作系统中存在这三种层次的回调

1、硬件设备往内核态的回调通过硬件中断实现

2、内核态往用户态的回调通过睡眠队列唤醒 + 进程切换实现

3、用户态中进程/线程往协程的回调通过协程调度器(epoll)实现


参考

微信 libco

一次系统调用开销到底有多大?

GCC 优化编译指南

]]>
如何正确新增字段 2021-04-01T00:00:00+00:00 http://huangtuzhi.github.com/2021/04/01/mysql-ddl 最近运维执行 DB 变更,给业务表 T_pay 新增字段 F_id,引起 DAO 模块读数据接口 GetBillInfo 出现大量找不到记录的失败。为什么只是新增 MySQL 字段,会引起模块接口失败?


1.原因分析

业务 MySQL 采用了读写分离的方式,写 Master 主机,读 Slave0 从机。这样的好处是读写负载被分布到不同机器,同时可以防止大量读引起 Master 故障。

看 DB 监控发现新增字段时,出现了大量的主备延时。什么是主备延时?

下图是主备同步的流程,Master 生成 binlog 后,dump_tread 线程将 binlog 同步传输给 Slave。Slave 上的 io_thread 线程接收数据并存储为中转日志 relog,最后由 sql_thread 线程将数据重放到引擎存储中。主备延时是从主机执行事务成功到备机 sql_thread 线程重放数据到最新事务 id 的时间差。

1.1.为什么 DB 新增字段会影响主备延时?

运维操作 DDL 变更时选择的策略是从主机变更,那么会先在 Master 进行 DDL 操作,再同步 binlog 到 Slave。

T_pay 表有 300M 大小,主库进行 DDL 时,会产生 mixed 的 binlog。从库 sql_thread 为单线程重放 binlog 日志,这个过程需要重建数据,占用线程时间长,造成 relaylog 堆积,产生延时。

1.2.主备延时会影响 master 上的写操作吗?

出现主备延迟只影响了 Slave0 上的 select,并没影响 Master 上的 insert、update。原因是主备延时不会影响半同步。

业务写 Master 时,会先写 binlog,只要有一个 Slave 返回 ACK,半同步就完成了。Slave 只要更新到 binlog 就返回 ACK 给 Master 了,然后 Slave上 的 sql_thread 进程可以慢慢重放数据。

1.3.出现读数据接口失败原因

用户先付款成功,会在 Master 上 InsertBillInfo 生成一条订单记录;然后在 Slave0 上 GetBillInfo 去读这条订单记录,但由于主备延时,这条订单记录还没有在从机上生成,所以出现找不到记录的失败。


2.如何变更

方案一:DAO 切换为写 Master 读 Master

优势:切换之后实时性更好,不会出现主备延时

劣势:DAO 在读写分离时,可以容忍读故障。例如,索引问题出现慢查询引发 DB 故障,此时只是影响读,业务可以正常运行。读写分离可以对这些场景进行兜底。如果切换为写 Master 读 Master,一旦出现故障,业务会不可用。可以考虑在读 Master 后变更 DB,观察业务情况后再判断是否需要切回 Slave。

风险:需要修改配置切换为读写 Master

方案二:低峰期从主机缓慢变更

优势:DB 变更已开始执行,这种方案不需要代码层面的修改

劣势:每个表之间的变更间隔必须加大,尽量让中间 sleep 的时间追齐前面表变更导致的延迟。不加间隔时大约需要执行 4 小时

风险:出现主备延时,导致销帐延迟

方案三:运维写脚本分开主备变更

优势:可以尽量减少备机延时

劣势:需要运维手写脚本来分开主备变更,属于非标准操作,风险较高

风险:运维手写脚本分开主备变更

方案四:从机读不到时再读 Master 兜底

优势:可以解决一部分主备延时问题

劣势:放大请求,有些订单本来就读不到

方案五:从机获取两个 Slave 的延时,选最优 Slave 读

优势:可以减轻主备延时

劣势:可能还是存在延时问题

方案六:低峰期挂公告停服再变更

优势:不需要开发和运维变更

劣势:MySQL 执行无法并行,执行时间 4 小时+,停服时间过长

风险:影响用户体验

结论

对比以上方案后,选择方案一进行变更。变更当晚又出现主备延时。但因为 GetBillInfo 读订单表已经切换到读 Master,所以不再出现读不到订单的问题。


3、主备延迟来源

在备库上执行 show slave status 命令查看 Seconds_Behind_Master 字段可以知道当前备库的延迟时间。

主备延时包含两部分时间损耗:主机传输 binlog 日志到备库;备机接收完 binlog 和执行完这个事务。在网络正常情况下,第一部分耗时很短,主备延迟主要由第二部分引起,也就是备库 sql_thread 线程消费 relay log 的速度比主库生产要慢。

除了上面业务中出现的场景会导致这种问题,还有哪些场景也会导致主备延迟?

1、备机机器性能比主机差,或者参数配置不同

2、备机负载压力大,比如在上面执行了数据统计分析语句、运维日常导出脚本

3、大事务,在主机上执行 1 min,在备机上可能重放数据也需要 1 min


参考

MySQL DDL 为什么成本高

MySQL 45 讲:MySQL 是这么保证高可用的?

]]>
TecentOS Tiny-从 0/1 到 IOT 设备 2020-10-14T00:00:00+00:00 http://huangtuzhi.github.com/2020/10/14/binary-to-IOT 随着网络通信技术发展,物联网在生活中应用越来越广。本文基于 TencentOS Tiny 物联网操作系统和 STM32 芯片,尝试从最底层的物理 0/1 高低电平到芯片寄存器,再到 RTOS 操作系统、甲醛传感器 IOT 组件,最后到腾讯云 IOT 平台来分析一个简单的物联网设备原理,更好的理解在嵌入式系统中软硬件是如何协作的。


一、TencentOS Tiny 架构

TencentOS tiny 主体架构图包含了这些

其中 CPU/MCU 常用的 ARM Cortex 系列分为下面几类:

  • Cortex-A 系列:面向性能密集型系统的应用处理器内核。应用在智能手机、上网本、数字电视等。
  • Cortex-R 系列:面向实时应用的高性能内核。应用在汽车制动系统、动力传输、航天航空等。
  • Cortex-M 系列:面向各类嵌入式应用的微控制器内核。应用在微控制场景、成本敏感性的产品。开发板所用的 STM32G070RBT6 就属于此类的 M0。

HAL 层

SMT32 MCU 的 GPIO(General-purpose input/output)引脚连接到外围电路,MCU 通过写 GPIO 的寄存器,可以将引脚设置为输入/输出、高/低电平。这样可以感知到外界物理电路高低电平变化或控制外接电路,从而实现控制(Microcontroller)。

比如,通过写寄存器直接将 GPIOB5 口的电平设置为高电平,从而点亮 LED。

GPIOB->ODR |= 1<<5; // 即 0x002333333 |= 1<<5
GPIOB->ODR &= ~(1<<5);

在开发中完全可以用裸的寄存器操作完成功能开发,但各种类型的芯片引脚、寄存器定义不同。如果能抽象出对 GPIO 的处理函数效率会更高,这就是 HAR(Hardware Abstract Layer)硬件抽象层。芯片厂家在这里 HAL_GPIO_TogglePin 函数封装了对 GPIO 的处理。


BSP 层

当我们尝试在 OLED 上展示 “Hello World” 时,需要频繁调用 HAL_GPIO_TogglePin。我们又可以将这些 GPIO 初始化、GPIO 电平设置的操作封装为一些函数,这样就形成了 BSP(Board Support Package)板级支持包层。TencentOS Tiny 在这里 OLED_ShowString 函数封装了液晶屏展示的功能。

板级支持包也可以看做显示屏/传感器/串口模块/WIFI 模块支持包,上层应用不需要关心底层的寄存器操作和电平变化。通过由下而上的 HAL 层、BSP 层,TencentOS tiny 将最下层的硬件层屏蔽起来,可以在此之上构建操作系统 Kernel。


操作系统抽象层

TencentOS tiny 实时内核包括任务管理、实时调度、时间管理、中断管理、内存管理、异常处理、软件定时器、链表、消息队列、信号量、互斥锁、事件标志等模块,实现了一个操作系统所有的机制。

为了形成完整的物联网组件 + 物联网操作系统 + 腾讯云 IOT 平台,定制化操作系统抽象层有很多优势。通过向下封装基础物联网组件 BSP,向上封装腾讯云 IOT 能力,如果形成行业统一的标准,会很大程度提升产品研发效率。


二、甲醛监测仪搭建步骤

主要需要把 TOS EVB G0 开发板、ESP8266 WIFI 模块、甲醛传感器搭建成甲醛监测仪,步骤如下:

1、通过串口下载固件到 ESP8266 WIFI 模块

ESP8266 模块本身也有 Tensilica L106 控制芯片,可以进行编程实现功能。TencentOS Tiny 已经提供了和腾讯云 IOT 平台对接的代码编译出来的 bin 二进制(固件),只需要将二进制烧录到芯片中就可以实现和腾讯云 IOT 的交互。TencentOS Tiny 做的工作是扩展封装了 ESP8266 的 AT 指令集,这样 STM32 芯片只需要通过串口把 AT 指令发送到 ESP8266 上,就能实现 STM32 和腾讯云 IOT 平台的通信。

在下载 bin 二进制时,需要把ESP_TXD 连接 USB 转串口芯片引脚 CH340_RX,ESP_RXD 连接 CH340_TX。这样 ESP8266 直接连接到 PC 串口 COM*,可以直接下载 bin 二进制。下载完后,恢复连接 1-3、5-7、2-4、6-8,让 ESP8266 和 STM32 相连,这样 STM32 才能控制 ESP8266 WIFI 模块。

2、通过 ST-LINK 下载程序到 STM32G070RBT6 芯片

使用 ST-LINK 调试器串行调试 SWD(Serial Wire Debug)模式来下载程序到芯片中。SWD 是芯片自身支持的调试能力。


三、代码分析

要理解整个嵌入式系统的原理,还需要分析一下工程的代码。整个工程路径在这里


应用入口

整个工程的入口函数是 application_entry,它对应的定义是:

extern void application_entry(void *arg);
osThreadDef(application_entry, osPriorityNormal, 
                       1, APPLICATION_TASK_STK_SIZE);
EMBARC_WEAK void application_entry(void *arg)
{
    while (1) {
        printf("This is a demo task!\r\n");
        tos_task_delay(1000);
    }
}

int main(void)
{
    /* OS kernel initialization */
    osKernelInitialize();
    /* application initialization entry */
    osThreadCreate(osThread(application_entry), NULL);
    /* start kernel */
    osKernelStart();
}

osThreadDef 将 application_entry 函数定义成一个 os_thread_def 线程结构体,osKernelInitialize 用来初始化内核,接着 osThreadCreate 创建 application_entry 为主体函数的线程。osKernelStart 启动 RTOS 内核。整个 RTOS 使用参考 CMSIS-RTOS API


主任务线程

OLED 显示、读取甲醛传感器数值、WIFI 通信的主要逻辑写在 mqtt_demo_task 中。

// 初始化 WIFI 模块连接的 UART2 串口
esp8266_tencent_firmware_sal_init(HAL_UART_PORT_2); 
// 加入 WIFI 网络
esp8266_tencent_firmware_join_ap("user", "passwd"); 

UART3 连接了甲醛传感器读取数值,当需要读取数值时需要使用 HAL_NVIC_EnableIRQ 打开 UART3 的中断 ,让 CPU 能接收中断。

UART 引脚连接的功能如下:

  • UART1:USB 转串口芯片 CH340 引脚
  • UART2:ESP8266 WIFI 模块引脚
  • UART3:E53 甲醛传感器底板引脚
  • UART4:P6 4 针插线柱

核心逻辑主要有下列几步:

1、读取甲醛传感器数据

读取甲醛传感器数据由 ch20_parser.c 中的 ch20_parser_task_entry 线程处理,读取完成后通过 tos_mail_q_post 发送到邮箱,主线程通过 tos_mail_q_pend 读取邮箱数据。

那么 ch20_parser_task_entry 线程中的数据来源于哪里呢?是在 stm32g0xx_it_demo.c 中 HAL_UART_RxCpltCallback 接收 CPU 的 UART3 中断回调时写入的。

2、通过 WIFI 上传数据到腾讯云 IOT

上传数据是通过 tos_tf_module_mqtt_pub 发送数据到队列,其实是把 AT 指令 + 数据写到 UART2 的 WIFI 模块,WIFI 模块再上传数据。

int esp8266_tencent_firmware_module_mqtt_pub(const char *topic, qos_t qos,
 char *payload)
{
    at_echo_t echo;

    tos_at_echo_create(&echo, NULL, 0, "+TCMQTTPUB:OK");

    tos_at_cmd_exec(&echo, 1000, "AT+TCMQTTPUB=\"%s\",%d,\"%s\"\r\n", 
            topic, qos, payload);
    if (echo.status == AT_ECHO_STATUS_OK || 
         echo.status == AT_ECHO_STATUS_EXPECT)  {
        return 0;
    }
    return -1;
}

四、从 0/1 到 IOT 设备

这样就完成了从硬件层 0/1 高低电平到 STM32 芯片,再到 RTOS 操作系统、甲醛传感器物联网组件,最后到腾讯云 IOT 平台的整个流程。

]]>
重构项目 2020-08-10T00:00:00+00:00 http://huangtuzhi.github.com/2020/08/10/refactor-project 最近这段时间工作重点是技术优化,包含代码圈复杂度、代码缺陷优化、项目重构等。重构的项目包括获取机构配置服务、离线对账脚本。获取机构配置每天 4000W+ 访问,对账脚本又涉及到大量资金,重构这些项目困难重重。


重构原因

既然服务这么重要,出错容忍性这么低,为什么要进行重构?

以获取机构配置服务为例,之前存储使用 MySQL, 保存了 5K+ 机构、1W+ 个性化配置信息。在业务高峰期 DB 物理机 CPU 接近满载。为了满足后续业务增长,需要将配置信息迁移到 KV 服务。KV 相比 MySQL 性能更好,同时支持多地容灾。重构之后收益较高。

再比如历史对账脚本使用了 Python、Shell,代码中硬编码了 MySQL 的 IP 端口。从安全和质量的角度来看存在很大问题。在部门的推进下,对这部分代码也需要做改造。

一般项目重构有这些原因:

1、系统容量无法满足业务增长,需要优化服务性能。系统不支持横向扩展,不能通过扩容解决。比如 MySQL 配置库读的是一组 DB,但 DB 的内存、CPU 会有上限。

2、系统架构满足不了新产品需求,需要重新设计增强扩展性。比如之前的业务逻辑是 hardcore,后面又需要针对不同的场景进行个性化,这样整个系统可能需要重新设计。

3、安全和质量原因,需要使用更规范的实现。很多项目在上线之初快速迭代,忽略了代码质量、安全风险。项目里大到架构设计,小到使用组件的方式、编码原则都可能都存在安全和质量问题。

4、旧组件下线和迁移,不可抗力,各种组织里反复造轮子屡见不鲜,旧轮子不维护,新轮子强制上线替换。


重构原则

在重构时,我们应该遵循什么原则来变更?下面对实践中遇到的问题和经验进行总结。

一、可验证原则

a. 变动逻辑的关键日志、监控,执行流应符合预期。比如新增一个逻辑分支,上报的监控变化一定和改动一致。

b. 新旧逻辑的结果对比,结果应完全一致。

业务代码里需要针对这种验证场景写对比逻辑,第一步双读加入监控进行对比,第二步等线上跑一段时间数据完全一致,再切换为新的读分支上线。

二、最小改动原则

如果改动过大,在排查问题时都无法用控制变量法筛选。

a. 另起炉灶,使用新的函数、文件重构。比如重构获取实名的函数 GetRealName,有多处地方调用。可以新写一个 GetRealNameV2 放到 GetRealName 函数中进行替换。

b. 现在的方案是否是最好的,能否废弃掉用更简单的方案。当项目越来越臃肿,可能背离了之前的初衷。回顾一下也许已经有了更简单的方案,使用这种方案也许是更小的改动方案。比如之前使用脚本和机构进行对账、退款,现在已经有了 API 的方式,机构直接使用 API 退款即可。

三、灰度升级原则

升级时需要灰度一部分流量到新服务进行验证,如果流量走到旧系统就使用旧服务,流量走到新系统就使用新服务。 比如实名验证服务升级时,新服务由 CGI 接入层加一个 tag=newrealname 传到后面 AO 层,如果是这个 tag 就使用新的 AO 服务。避免升级出现问题影响太大。

四、可回退原则

a. 重构服务上线后出现异常,应能较快切换到之前的版本。这种机制由变更工具/框架/业务幂等逻辑保证

b. 回退期间的异常脏数据修复。重构服务上线回退时可能出现脏数据,需要提前想好回退后的修复方案。修复方案设计不合理也可能出现问题,需要针对修复方案再修复,套娃开始…


上线 check 事项

  • 机器负载(CPU、内存、IO、GPU、硬盘)性能变化
  • 现网服务监控曲线变化
  • 关键业务日志执行流是否符合预期
  • 热更新 or 冷更新,能否直接替换(例如执行中的脚本不能直接替换)
  • 脚本文件替换后,是否有执行权限
]]>
如何进行技术面试 2018-10-03T00:00:00+00:00 http://huangtuzhi.github.com/2018/10/03/how-to-interview 前段时间帮项目组面试了一些应届毕业生,在写代码的间隙和这些快毕业的年轻人交流也学到了很多。针对面试中遇到的问题,写一篇文章总结一下如何进行技术面试。

一、简历

简历很重要,排版和文字体现了态度和专业程度,写到上面的项目和技能决定了面试方向。

1、简历排版

排版的基本原则

a) 对齐。简历整体应保持一个对齐样式。

b) 字体和色彩保持一致,最好不要出现三种以上的色彩和字体。

c) 页数为一到二页,大多数好的简历都是一页写完。便于 HR 和技术面试官筛选。

排版工具不限,Word、Markdown、网页都可以,推荐使用 Latex,Latex 是出版公司使用的专业排版软件,高度自由、专业大方。其他排版原则可以参考「写给大家看的设计书」。

2、定制化内容

对应于技术岗位要求,不要出现过多非必须的描述。如:学生会干部经历、个人爱好、甚至照片都是非必要的。应该有的是实习经历、项目经历、竞赛经历等。

3、扩展项

简历可以表达的内容有限,又是静态的。可以通过富媒体扩展引入

a) 个人博客网站。个人博客最好是多年有高质量内容更新的,面试官可以通过博客了解到面试者学习关注的方向,对技术问题的思考和总结。推荐使用 WordPress/Github Pages 等工具自己搭建。

b) Github。最好是有高 Star/Fork 的个人项目,体现技术影响力。面试官可以了解到编码风格、软件工程素养。

c) 项目演示 Demo。

二、面试考察项

1、基础知识

最基本的岗位能力要求,包括语言基础、操作系统、计算机网络、数据库、数据结构、软件工程、数据技术等等。至少需要对某一个方向有较深入的研究和思考。

2、聪明程度

这是一个主观又难以量化的指标,当面试官问出一个问题时,他是知道答案的。他可能只是简单看过答案,或者做项目时遇到这个问题,或者刚好思考过这个问题,而对面试者来说可能完全是陌生的。这样来看其实不公平,用一个比较少遇到的问题去考察其他人,这种问题也不能测试出面试者的聪明程度。那如果出的题是从简历里引申出来又稍微有深度和难度的呢。这时就可以看到面试者的应变能力和知识迁移能力。用这两个指标去衡量时会更有效一些。举例:

「看你简历上写的了解 Linux 文件系统,如何在终端里删除一个名字为乱码的文件」,这个题目里有明显提示-Linux 文件系统,面试者也有这个基础能力,他如果能想到文件的 inode 数据结构,再结合管道就可以答出这道题。

「看你项目中提到了协程,怎么去设计一个协程库」,面试者可能没有看过协程源码,但知道基本原理后结合面试官的提示是可以推导答出大概的框架。

3、沟通能力

沟通更多是一种方法。

a) 是否可以做到结论先行。回答问题时,先明确给出答案,再给出推导过程。

b) 是否可以把回答归纳总结为有条理的子项。归纳的过程也是展示思考的过程,原则是子项独立不重复;整体不遗漏。

c) 是否可以把一个其他人不熟悉的概念用简洁易懂的语言向他讲明白。我们可以用这条标准来衡量自己是否真正理解一个概念,那就是把这个概念给完全不懂的人讲清楚。

d) 在传达信息时,是否可以及时调整表述,以确保信息被正确接收到。「项目计划」中提到,作为项目经理,我们必须要记住一个基本前提,就是防止误解是信息发送者而不是接收者沟通职责。

其他沟通方法可以参考「非暴力沟通」。

三、面试官与面试者

管理大师德鲁克说,在组织中的人既是管理者,也是被管理者;既是管理的主体,同时又是客体,都承担双重角色。而面试官与面试者也是这种关系。一方面,面试官承载着这家公司的品牌形象和文化,也在接受面试者的评价和选择。另一方面,面试官极有可能有一天也是要去面试的。那么从这两个方面,作为普通技术人员的面试官应该做些什么?

1、面试官的职责

a) 提前 review 简历,针对提到的技能和项目,定制化一份可以综合评价能力的面试题

b) 平等认真倾听和记录面试者作答,引导面试者展现能力

c) 向面试者介绍公司和项目组在做的事情

d) 面试者有主动寻求建议,应适当给出建设性的答复

2、技术人员出走的能力

面试官作为一个有较多工作经验的普通开发人员,也会有一天转换角色成为面试者。为了适应这个激烈变化的互联网行业,他们应该保持随时出走的能力。开发工程师平时可以关注这些:

a) 提高业务能力和积累高价值的项目经历

b) 维护更新一份自己的简历

c) 学习新的语言和技术,创建自己的项目和产品

d) 积极分享,提高自己在业界的影响力(写公众号、博客、Github、开发者社区)

e) 主动了解业界情况(薪资待遇、发展机会、业务范围)

f) 有机会的话与猎头维持良好的联系

最近公众号更新较少,一些想法记录在这里,一为总结分享,二为自察勉励。

]]>
遇到的加密算法 2018-10-01T00:00:00+00:00 http://huangtuzhi.github.com/2018/10/01/encrpt-algorithm 最近开发项目,遇到了各种见过没见过的算法。总结整理一下。


AES 算法

AES 是对称加密算法,也就是用相同的秘钥加密和解密。它有这些特点:

1、是分组加密,每个加密块大小为 128 位,即 16 个字节。

2、秘钥是 128/192/256 位。秘钥一般写为十六进制的 hex 格式,128 位秘钥就是 32 位的 hexcode。进行运算时需要将 hexcode(string 类型) 变为二进制字节流(char[16]类型)。

3、AES 有四种加密方式,常用的有 ECB 模式和 CBC 模式。


ECB 模式

ECB 指电子密码本模式 Electronic codebook。

AES 是分组加密,以 16 个字节为一组,如果加密数据最后一个组不够 16 个字节,就需要填充为 16 个字节。这个填充方式就叫 padding。padding 方式有 zeropadding/pkcs5padding/pkcs7padding。

ECB 是最简单的 AES 算法,用秘钥分别对填充后的分组数据进行加密得到密文。所以密文和明文的长度成正比。

使用 OpenSSL 库的 API 如下:

#define COMM_AES_BLOCK_SIZE 16

int AES_ECBEncrypt(const char * sSource, const int iSize,
		const char * sKey, int iKeySize, std::string * poResult)
{
	poResult->clear();
	int padding = COMM_AES_BLOCK_SIZE - iSize % COMM_AES_BLOCK_SIZE;

	char * tmp = (char*)malloc(iSize + padding);
	memcpy(tmp, sSource, iSize);
	memset(tmp + iSize, padding, padding);
	poResult->reserve( iSize + padding);
	unsigned char key[COMM_AES_BLOCK_SIZE] = {0};
	memcpy(key, sKey, iKeySize > COMM_AES_BLOCK_SIZE 
	? COMM_AES_BLOCK_SIZE : iKeySize );

	AES_KEY aesKey;
	AES_set_encrypt_key(key, 8 * COMM_AES_BLOCK_SIZE, &aesKey);
	unsigned char out[ COMM_AES_BLOCK_SIZE ] = { 0 };
	for (int i = 0; i < iSize + padding; i += COMM_AES_BLOCK_SIZE) {
		AES_ecb_encrypt((unsigned char*)tmp + i, out, &aesKey,
		AES_ENCRYPT);
		poResult->append((char*)out, COMM_AES_BLOCK_SIZE);
	}
	free(tmp);
	return 0;
}

CBC 模式

CBC 指密码分组链接模式 Cipher-block chaining。

CBC 相比 ECB 会复杂一些,它将上一次加密得到的结果与本次的数据块异或之后再进行加密。这样还需要一个初始的异或数据,叫做初始化向量 IV。

使用 OpenSSL 库的 API 如下:

static const int COMM_AES_BLOCK_SIZE = 16;
static const int COMM_AES_IV_SIZE = 16;
static const int COMM_AES_KEY_SIZE = 16;
static const int COMM_AES_PADDING_SIZE = 16;

int AES_CBCEncrypt( const char * sSource, const int iSize,
		const char * sKey, int iKeySize, std::string * poResult )
{

	poResult->clear();
	int padding = COMM_AES_PADDING_SIZE - iSize % COMM_AES_PADDING_SIZE;

	char * tmp = (char*)malloc( iSize + padding );
	memcpy( tmp, sSource, iSize );
	memset( tmp + iSize, padding, padding );
	
	unsigned char * out = (unsigned char*)malloc( iSize + padding );

	unsigned char key[ COMM_AES_KEY_SIZE ] = { 0 };
	unsigned char iv[ COMM_AES_IV_SIZE ] = { 0 };
	memcpy(key, sKey, COMM_AES_KEY_SIZE);
	memcpy(iv,"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30 /
		\x30\x30\x30\x30\x30", 16); // 这里按照约定设置 

	AES_KEY aesKey;
	AES_set_encrypt_key( key, 8 * COMM_AES_BLOCK_SIZE, &aesKey );
	AES_cbc_encrypt((unsigned char *)tmp, out, iSize + padding, 
		&aesKey, iv, AES_ENCRYPT);

	poResult->append((char*)out, iSize + padding);
	free( tmp );
	free( out );
	return 0;
}

Java 的示例代码见 aes-encoder


银行通用 MAC 加密算法

银行 ATM 通信经常使用 MAC 加密算法来当签名,它们会使用加密机来生成这个签名。这次遇到的机构使用卫士通 SJL05 型金融数据加密机。而我们使用软加密的方式来模拟加密机进行加密。


加密机原理

加密机有两个秘钥:主秘钥和工作秘钥。我理解的加密机工作流程是这样的:用户任意输入一个工作秘钥 A,加密机对 A 使用主秘钥 B 进行 ECB 加密得到 C(PMAK),然后对 C 进行 3DES 加密得到 D。D 会作为和加密机通信的秘钥 MAK,作为通信报文字段。

因此,软加密需要用加密机主秘钥 ECB 加密工作秘钥,得到明文加密秘钥(PMAK)。即软加密 MAC 算法中输入的秘钥。


MAC 算法

1、将需要加密的数据按照 8 个字节分组(不满 8 个字节则进行填充),然后两两异或,最终得到 8 字节的数据

2、将 8 字节的数据转换为 16 长度的 hex 十六进制数据

3、对十六进制数据进行 CBC 加密得到最终 MAC


参考

AES 的工作模式

SJL05金融数据加密机程序员手册

]]>