作者归档:Young

基于数据库实现幂等接口

 

TL;DR

通过唯一编号确定同一请求,没有唯一编号的自行生成。

数据库记录操作状态,数据库事务保证数据一致性。

概述

通过HTTP API进行通信的系统,在支付或者只允许操作一次的相关场景中,对接口的幂等性有严格要求。

接口的幂等性体现在:

请求执行成功所得到的结果与次数无关

如果接口没有实现幂等性,对于转账的应用场景:

A. 正常转账

  1. A账户金额为¥200,B账户金额¥100
  2. A调用API向B转账¥100,接口调用成功
  3. A账户金额为¥100,B账户金额¥200

在这一场景下,整个流程正常,接口无论是否实现幂等性与否都对执行结果没有影响。

B. 转账重试

  1. A账户金额为¥200,B账户金额¥100
  2. A调用API向B转账¥100,接口调用失败
  3. A重试请求,本次成功
  4. A账户金额为¥100,B账户金额¥200

在这一场景,重试成功的情况下,接口无论是否实现幂等性与否都对执行结果没有影响。

C. 转账操作超时后重试

  1. A账户金额为¥200,B账户金额¥100
  2. A调用API向B转账¥100,接口调用超时,A不了解本次转账是否完成,服务端实际转账成功
  3. A重试请求,本次成功
  4. A账户金额为¥0,B账户金额¥300

在实际应用场景中,接口超时的情况并不罕见,接口超时不代表操作失败,可能存在的情况就有操作实际成功然而并没有返回数据。在这样一个场景之下,接口没有实现幂等性造成重复操作,对于系统的可靠性来说是不可容忍的。

D. 重复的转账操作

  1. A账户金额为¥200,B账户金额¥100
  2. A调用API向B转账¥100,由于A的误操作发出了第二次同样的请求
  3. A发出的两次请求均成功
  4. A账户金额为¥0,B账户金额¥300

两次操作同时发出,并且都成功,接口没有实现幂等的情况下,两次转账操作都会成功,但是对于用户A来说,实际上这是同一次的转账意愿。

以上的场景还是在A与B账户均存在于同一个资源(一般为数据库)之上的操作,如果A与B账户处于两个资源,场景还会更加的复杂。

由上述的场景可以看出,实现接口幂等性的两个方向在于:

  • 定义同一次操作
  • 拒绝重复操作

实现

利用数据库实现上述两个需求十分方便。

定义同一次操作

使用数据库实现发号器,为每一次请求生成唯一编号

拒绝重复操作

通过数据库事务以及唯一索引,以请求编号作为依据,保证同一时间内只有一个请求进行操作,经过先查询后操作的方式,已完成操作不执行更改逻辑,保证请求值执行一次。

以MySQL为例,针对需要实现幂等的操作,可以建立如下的数据表:

其中op_no列存在唯一索引。

针对上述转账的场景,设定A与B都处于同一数据库中,可以用如下伪代码表示上述的转账操作:

以上针对于只有一个业务方/使用者的场景,如果有多个业务方的情况下,只需要在幂等操作表中增加一个来源字段(如名为source),并对source字段与op_no做联合的唯一索引即可。

事实上,在所有操作都带有前置状态的情况下(即所有改动都显示的指明上一个状态),如果接口操作只有一步,而没有多个步骤需要同时成功失败的情况下,甚至不需要显式的开启事务。

以上。

Next主题中从服务器加载Google字体

 

概述

想要在Hexo的Next主题中使用Google字体,一个问题在于这个字体的加载并不是很方便(与Next主题本身无关)。

解决的方法可以是修改主题,在没有合适CDN的情况下,从服务器直接加载对应字体也是一种选择。

实现

准备字体

首先,通过Chrome开发者工具,会发现难以加载的是这一css文件以及相关联的字体:

当前使用的Next主题使用的是Lato这一个字体,那么应该如何得到这一字体呢?

这时候就需要Google WebFont Downloader了。

如果有Node环境,那么直接执行:

即可。

使用上,可以通过-h查看参数定义:

通过上面的url,可以确定使用的的字体为Lato,那么下载相同的字体需要指定的参数为:

这里-a参数保证下载了所有字体。

如果可以顺利的访问对应的资源,那么成功之后将会在当前目录出现字体名为名字的css以及字体目录。

修改主题

对应的域名是googleapis,可以考虑在Next主题的vendors目录下新增一个同名的目录,将css以及字体组织。目录层级会变为:

剩下的工作是,在加载原先的css的partials中将url改成加载verdors中对应的地址即可:

当然,如果不想这么麻烦,直接在主题设置中关闭Lato字体即可。

PHP使用共享内存

 

概述

共享内存指在多处理器的计算机系统中,可以被不同中CPU访问的大容量内存。是众多进程间通信(IPC)方式中最快的一种,无需进行数据拷贝等操作,即可在各个进程之间共享数据。

PHP同样也提供了对共享内存操作的可能性,通过shmop扩展实现。

应用

结合一个实际使用的场景,PHP daemon多进程从上游获取用户ID,需要与指定数据集去重,用户ID大约为12位的数字。要求是不能多去重(即不能存在误伤的判断),但是也不能少去重。

考虑到多进程处理时,需要考虑如何实现便捷,快速的进行判断用户ID已存在于数据集之中。在数据结构上可以使用的几种方法有:

  • Bitmap
  • Hash
  • Array

下面分别说明三者的优缺点:

Bitmap

Bitmap(位图,以下不加区分的使用)的特点在于数据密度大时(即已排序的情况下,相邻数字间隔不大),是极为节省内存用量的数据结构。无论数字多大,在内存中只通过1 Bit进行表示。假设用1 Byte的空间进行数字的表示,数据集为[1,3,4,5,7],则这一组数据的位图可以表示为:

即便数字相当巨大,在已知最小值的情况下,同样通过同样大小的空间也可表示,如数据集为[100000001,100000003,100000004,100000005,100000007],同样也可以用同样一个位图进行表示,因为所有数字的都可以认为相对于100000000进行了偏移操作。

Bitmap操作起来速度与便利性也相当令人满意,只需要根据数字大小,找到对应的位,判断当前位的0/1值即可,位运算操作的速度之快无需多言。

然而,Bitmap的最大问题在于,如果存储的是数值的顺序信息,那么整个Bitmap的数据才是最有效的。即,如果已经数字本身是有序的,如从1开始,一直到10000,或者是有办法迅速的知道位置是1的数字的具体字面值,那么存储的位会更加的高效。

在存储用户ID这一个场景时,Bitmap不一定适用,因为用户ID可能会长于10位,如果把用户ID当成数字来看,同时考虑到可能存在的一些业务形态(6,8之类的靓号逻辑,4之类的避讳逻辑),可能得到的Bitmap就相当的“稀疏”(0过多,1过少),造成的结果就是内存的有效使用率降低。假设用户ID最大值是9999999999,仅仅在10位数的用户ID的情况下,为了包含所有的数字,需要开辟约1192.09MB大小的内存空间,当然实际应用中很可能会比这个要小,通过找到偏移值(获取比较集的最大最小值,确定偏移值,减少无用0的内存占用)、数据分块(比如前5位相同的比率大,把数字前5位作为一个集合,只生成后5位的Bitmap提高表示有效程度)等方式,但是又会引入一些其他的问题,或者效果不佳(比较集合中存在1和9999999999两个用户ID)或者是难于管理(前5位有上万种组合

)。

如果在可以接受误伤的情况下,有一个更优的方案,即布隆过滤器(Bloom filter),这是通过Bitmap可以完成的,综合速度和存储压力都较优方案。

Hash

Hash(哈希表)的特点在于快,理论上来说,Hash的查询和写入时间复杂度都是O(1)

编程语言如果提供了Hash的数据结构,那么应用数据结构就成为了一个看起来不错选择了。

实际实现中,Hash存在的第一个可能存在的问题是Hash冲突的解决,如果用开放地址法(Open addressing)进行解决,可能的问题在于冲突之后查询下一个可用地址的次数过多,而用链表(Separate chaining)解决,则会存在退化的情况(所有值都hash到一个链表中)。不过这些问题一般来说都应该是在应用过程中由编程语言关心的。

这次应用限定了编程语言为PHP,那么从PHP的实现上来看看应用Hash是否可行。

PHP里的数组就提供了Hash的功能,PHP数组的便利程度无需多言,在去重这一个场景上,完全可以通过用户ID作为key,写入布尔值作为value,通过isset()方法快速的进行去重操作。

速度上我们可能不再担忧了,但是内存占用上呢?

目前PHP的最新版本为PHP 7.1.0,常规编译安装后,通过如下脚本获取0~1000000在数组中的内存占用情况:

运行结果为:

仅仅1000000的7位字符串作为hash key的数据集,就需要耗费超过341MB的内存。可是即便是作为文本文件,这些数据集在通过换行符号分隔的情况下,完全加载到内存只需要大约8MB的内存使用。是什么造成了如此大的差距呢?

从PHP的源码来看(源码目录下的Zend/zend_types.h),PHP数组通过HashTable这一个struct实现:

数据的实际存储部分即Bucket,对内存占用影响起到决定性作用的也正是Bucket这个数据结构,Bucket的定义为:

Bucket中包含zvalzend_ulong,以及zend_string三种数据结构,下面分别来看看这几个数据结构。

zval

zval的定义如下:

zval中又包含zend_vlaue,那么通过zend_value的定义:

可以看出zend_value作为一个union至少要占用8个字节(最大的内存占用来自于当中包含的zend_long,在Zend/zend_long.h中被定义为typedef int64_t zend_long;),所以,作为一个struct,zval会占用sizeof(value)+sizeof(u1)+sizeof(u2)=8+4+4=16 Bytes。

zend_ulong

Zend/zend_long.h中被定义为typedef int64_t zend_ulong;,所以会占用8 Bytes。

zend_string

来看zend_string的定义:

其中zend_refcounted_h的定义为:

可以看到zend_refcounted_h的空间占用为sizeof(refcount)+sizeof(u)=4+4=8 Bytes。

size_t这里需要注意,因为在64位OS上编译,这里会占用8 Bytes。

结构成员val用于存储key的值,由于key都是7位长的字符串,所以这里会占用7 Bytes。

所以,在这里,空间占用为8+8+8+7=31 Bytes。

综上,直接统计数据结构的大小已经能明显看到PHP提供Hash是相当占用内存的,出于这个方面的考虑,基本可以认定Hash不适合当前的应用场景。

Array

可能有人会说,Array算是什么办法,但是对于特定情况,Array确实可以使用。

比如C语言的数组,是在内存中的一片连续空间,也就是说,实际上内存的占用就是数据个数乘以单个数据需要占用的空间。从这个角度来看,是没有太多的内存浪费的。

当然PHP的数组不能这么看,因为PHP的数组仍然有太多的冗余信息。

数组的“缺点”在于查找的耗时。线性查找的耗时几乎让人无法接受,那么如果数据可以是有序的,通过二分查找耗时将会大大降低。

1M的数据,只需要通过至多10次查找即可判断对应的值是否存在。

Bitmap/Hash/Array?

从数据特点和存储用量来考虑,同时考虑到查询速度,Array在这个场景下胜出。

存储

PHP可以使用共享内存,作为最简单的IPC方式,并且使用方式相当简单,PHP的shmop扩展中提供了对共享内存的操作能力。

创建/打开

共享内存在PHP的创建和打开工作是通过shmop_open方法实现的。定义如下:

PHP的共享内存的创建实际上是通过shmget这一系统调用实现的,参见shmop扩展源码:

shmget返回的值是一个类似文件描述符的存在,因为它并不是一个真正的文件描述符,所以我们实际上的操作依据仅仅上是一个全局唯一的数字,用来表示共享内存。同时通过shmat系统调用将共享内存映射到当前进程的地址虚拟空间之中。

共享内存打开方法中的第一个参数指定的key,是标识这一共享内存片段的依据,需要全局唯一,一个方法就是通过一个确实存在的文件,利用ftok系统调用,生成一个全局唯一的key。文件名不是决定key值的决定因素,决定因素是文件的inode号。

关闭

关闭的实现的是通过shmdt系统调用完成的。在扩展的MINIT阶段,通过zend_register_list_destructors_ex方法注册了资源析构方法为rsclean,对共享内存的的ID看做是资源(Resource)。

在调用这一方法时,通过资源删除APIzend_list_close调用注册的rsclean方法完成对资源的释放。

rsclean的实现如下:

对共享内存ID进行了shmdt操作,同时释放了的申请共享内存操作结构体。

删除

这一个方法中,是shmctl系统调用的表现的时候了。删除一段共享内存只需要将系统调用中的第二个参数设定为IPC_RMID即可。

当然,删除共享内存也可以通过Linux中的ipcrm命令完成,如ipcrm的man中提到的,如果知到key(即创建/打开部分提到的共享内存全局唯一的标识)则使用-M参数,知道ID则使用-m参数。

读取的方法则是通过共享内存ID以及开始位置以及读取的长度获得一个字符串。

从实现上来看,以下几种情况会返回false并打印WARNING日志:

  • 起始值小于0或者大于共享内存的容量
  • 读取的字节数小于0
  • 起始值大于INT_MAX与读取字节数只差
  • 起始值与读取字节数之和大于共享内存大小

实现上来说,实际上是通过memcpy将字符串的值复制到共享内存的指定位置。

参考

2016

 

工作

技术

年初仍然在维护一个基于Redis队列通信的数据筛选系统,做了较多可用性和可维护性方面的优化,通过状态埋点以及编写相关工具,让服务对于开发人员来说更加易于了解运行状态,使用方提出的问题也更加的便于排查。同时通过调整系统架构,将队列资源占用降低了50%以上。

年中作为后端工程师参与了一个售卖系统的部分核心服务的构建工作,对需要对接支付系统中的一些需要注意的地方有了一些了解。应用过程中对于事务有了更多的应用和了解,当然,还需要对分布式系统以及分布式事务做更多的学习。

全年都在支持整个部门的一些与运维对接的工作,几乎每天都会处理日志、服务器配置、域名解析等相关的日常事务性工作。维护部门内ELK日志平台,同时也配合业务同学开发了一些辅助工具。

Android 7.0之后没有及时跟进发展,本年Android开发学习计划有所停滞。

4月开始尝试重新学习算法,通过LeetCode做题方式进行学习,完成80题,未能达到定下来100题的目标。

生活

观影近年新高,总计136部,意识到了学习好一门语言的重要性,靠“听”来观影,可以在有限的时间内享受更多观影的乐趣,能利用这些时间做更多的事情。

阅读方面相比去年做的更不好,读书16本,虽然年内阅读的主动性有所提升,但是读书的本书却变得更少,实在是无法理解。

今年最大收获是培养出了跑步的爱好,同时也成功的减掉10KG,达到了预期的目标,身体状态相比去年好了很多。

摄影方面的学习有所停滞,快门数进展不多,明年应该会多拍一些吧。

大概这就是我的2016。

《微服务设计》

第1章 微服务

微服务就是一些协同工作的小而自治的服务。

大多数系统强调高内聚,低耦合,即通过抽象或者模块保证代码的内聚性。

一个基本的特征就是微服务是一个独立的实体。无论是在容器还是在进程中存在。

微服务通过API进行解耦合。

微服务的优点集中在:

  • 技术可以异构
  • 更大的弹性(可操作的粒度更大)
  • 易于扩展(精细到功能级别)
  • 简化部署(API接口不变,改动一个模块影响不大)

第2章 演化式架构师

架构师的一个重要职责是,确保团队有着共同的技术愿景,以帮助我们向客户交付他们想要的系统。

架构师要改变那种从一开始就要设计出完美产品的想法,而选择设计出一个合理的框架,在这个框架下可以慢慢的演化出正确的系统,一旦学到了跟过的的还是,可以加以使用。

架构师的职责之一是保证该系统适合开发人员在其之上工作。

架构师专注大方向,在优先的情况下参与到非常细节的具体实现上。

架构师关注服务边界之间的问题,而不应当过多关注边界内部的问题。

一个好服务至少要做到如下三个方面的优势:

+ 监控

+ 接口

+ 架构安全性

COBIT(Control Objectives for Information and Related Technology)给出的的治理的定义是:

治理通过评估干系人的需求、当前情况及下一步的可能性来确保企业目标的达成,

通过排优先级和做决策来设定方向。对于已经达成一致的方向和目标进行监督。

第3章 如何建模服务

服务要高内聚,低耦合,划分微服务的一个方式可以是找到各自功能的限界上下文(Bounded Context)。

第4章 集成

集成的几个原则:

  • 技术的选择需要有收益
  • 避免破坏性修改,对服务的修改不影响已有消费方
  • 保证API的技术无关性,应该是需求驱动实现,而不是实现驱动需求
  • 消费方易于使用,尽量不引入其他会引起耦合的使用方式(如client)
  • 隐藏内部实现细节

共享数据库会带来的问题:

+ 所有使用者需要了解schema之间的细节

+ 消费方被绑定了技术

基于请求/响应模式的同步请求易于实现,而基于事件的异步请求模式则能应对长时操作以及降低耦合度。

编排(Orchestration)与协同(Choreography)的区别在于是否有有中心。编排会通过中心驱动流程。编排优点是状态和流程明确,问题在于中心负担过重,导致其他协作方过于单薄;协同的优点在于解耦合,问题在于需要额外的监控流程。异步是便于实现协同模式的通讯方式。

对于基于事件的异步协作方式,需要关注的地方在于事件的发布机制和接收机制。发布机制中需要注意的有:

+ 消息中间件要尽量简单,不要混杂业务逻辑

服务即状态机。

权衡DRY与微服务过程耦合的冲突,原则上是微服务内部DRY,跨服务可以适当违反DRY。

服务客户端的开发的要点是:

+ DRY

+ 处理与服务本身职责没有关系,但是又影响服务大规模运行部署的一些基础功能的部分(服务发现,故障模式,日志),只包含处理底层传输协议的代码

+ 由客户端决定升级时机

RPC与REST相比,客户端和服务端的部署无法分离。

第5章 分解单块系统

“接缝”的定义是,系统中可以抽取相对独立的一部分代码,这部分代码进行修改不会影响系统其他部分,是划分服务边界的依据。

分块的一些实例:

+ 外键约束通过业务逻辑实现

+ 对于共享的数据,通过单一服务单元进行操作

+ 共享的数据库表,拆分字段单元

+ 报表的导出,要么通过异步逻辑(提交请求异步导出),要么用一些软件(如列数据库),或者独立程序定期生成报表数据到其他数据库(类似InfluxDB中INTO的表现)

第6章 部署

每个微服务都建议有自己的CI流程。

如果可能,应该将每个服务都放到单独的主机或者容器之中,

部署的关键在于各个步骤的自动化。

第7章 测试

测试时主要关注对场景的测试,而非面面俱到。

想要频繁的发布版本,需要尽可能频繁的发布小范围的改变。

蓝/绿部署和金丝雀发布的区别在于,蓝绿是切全部流量,金丝雀发布是引导部分流量。并且Canary版本验证的内容会包括功能与非功能的,两个版本共存时间更长。Canary版本的好处在于可以对效果做更多的干预,通过实际运行效果评估开发的效果。

微服务中MTTR表现良好胜于MTBF。

第8章 监控

Web提供给监控系统的指标数据,最低要求就是提供响应时间错误率

为了便于监控系统追踪请求,可以使用全局ID的方式,贯穿整个请求的流程。

对于系统来说,对于数据的聚合,可以:

+ 聚合CPU一类的的主机层级的指标及应用程序程标(帮助找到程序性能瓶颈)

+ 要能回溯存储数据(Nagios的瓶颈,存储时间太短,可以加入第三方的存储组件)

第9章 安全

交给单点登录网关的应该是粗粒度的身份认证,而系统级别/业务级别的认证控制,应该在微服务内部实现。

权衡服务间的信任问题,可以根据操作的敏感程度,从低到高选择边界内隐式信任验证调用方身份(验证access_token?),要求调用方提供原始主体凭证(如支付宝支付密码?)。

不要自己实现加密解密!不要发明自己的安全协议!要用行业的通用方式。

Datensparsamkeit:只存储完成业务运营或者满足当地法律所需的信息。没有存储,就没有丢失。

第10章 康威定律和系统设计

康威定律:任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。

团队结构影响开发成果(反康威定律也是有可能的)。

第11章 规模化微服务

设计微服务时的态度,就是假设一切都会失败。

权衡实现的复杂度,可以通过可接受的损失程度确定。

考虑跨功能服务发生故障时的场景,能明确定位和错误处理的实现。

级联的架构,必须要有保护机制,防止全站不可用。

主动制造故障驱动系统强壮性的增强(因人而异,无需极端追求)。

尽力追求操作幂等。

作业与执行逻辑分离,即通过worker实际执行任务,虽然可能中断或者执行缓慢,但是不至于造成任务丢失。

CAP中只能存在AP系统和CP系统。AP系统要实现最终一致,而CP系统通过拒绝服务保证C。CA系统因为牺牲了分区容忍性,根本不能跨网络运行,在分布式系统中,这完全与分布式不符合。

服务发现是规模化的一个重要组成部分。常规方案有:

+ DNS,通过名字引导流量到服务,缺点在于灵活性以及时效性不足

+ 动态服务注册,通过ZK等软件

第12章 总结

一切去中心化是微服务一个重要原则。

不了解系统承载的业务和特点之前,不要微服务化!不能自动化,不要微服务化!

> 变化是无法避免的,所以,拥抱它吧!

作者推荐书单:

其他关键词

  • CQRS(Command-Query Responsibility Segregation)