首页
影视
壁纸
留言
关于
今日热榜
推荐
音乐解锁
听歌
阅读器
听歌2
小乞丐
摸鱼
Search
1
Navicat Premium for Mac 中文破解版 (强大的数据库管理工具)
9,647 阅读
2
植物大战僵尸中文版 for Mac (兼容 M1系统) 中文版
5,774 阅读
3
CleanMyMac X 4.15.2 中文破解版 (Mac优化清理工具)
3,303 阅读
4
PDF Expert 中文破解版 (好用的PDF编辑器)
3,050 阅读
5
Parallels Desktop 17 中文版 (PD虚拟机无限试用版本)
2,824 阅读
生活杂记
macOS
编程技术
奇技淫巧
音乐
视频
δ
Search
标签搜索
python
漫画
php
mac
redis
mysql
mac 软件
macOS
音乐
极客爱情
数据库
游戏
吊打面试官
2.0
面试
罗小黑
linux
纯音乐
使用教程
B站
Kain
累计撰写
210
篇文章
累计收到
26
条评论
首页
栏目
生活杂记
macOS
编程技术
奇技淫巧
音乐
视频
δ
页面
影视
壁纸
留言
关于
今日热榜
推荐
音乐解锁
听歌
阅读器
听歌2
小乞丐
摸鱼
搜索到
68
篇与
的结果
2021-04-07
80% 的人都不会的 15 个 Linux 实用技巧
熟悉 Linux 系统的同学都知道,它高效主要体现在命令行。通过命令行,可以将很多简单的命令,通过自由的组合,得到非常强大的功能。命令行也就意味着可以自动化,自动化会使你的工作更高效,释放很多手工操作,让你有更多的时间去做更有意义的事情。这篇文章,会分享一些非常实用小技巧,希望能够帮助你提高工作效率,学完就能够用得上!1. 快速清空文件的方法快速清空一个文件,有 N 种方法,我比较喜欢下边这种,因为它最短$ > access.log不过瘾?好吧,我也顺便总结下,其它几种最常见的清空文件的方法: > access.logtrue > access.logcat /dev/null > access.logecho -n "" > access.logecho > access.logtruncate -s 0 access.log简单解释下, : 在 shell 中是一个内置命令,表示 no-op,大概就是空语句的意思,所以 : 的那个用法,就是执行命令后,什么都没有输出,将空内容覆盖到文件。2. 快速生成大文件有时候,在 Linux 上,我们需要一个大文件,用于测试上传或下载的速度,通过 dd 命令可以快速生成一个大文件$ dd if=/dev/zero of=file.img bs=1M count=1024上述命令,生成一个文件名为 file.img 大小为 1G 的文件。3. 安全擦除硬盘数据介绍一种擦除硬盘数据的方法,高效,安全。可以通过 dd 命令,轻松实现:$ dd if=/dev/urandom of=/dev/sda使用 /dev/urandom 生成随机数据,将生成的数据写入 sda 硬盘中,相当于安全的擦除了硬盘数据。当年陈老师,如果学会了这条命令,可能也不会有艳兆门事件了。4. 快速制作系统盘在 Linux 下制作系统盘,老毛桃神么工具都弱爆了,直接一条命令搞定:$ dd if=ubuntu-server-amd64.iso of=/dev/sdb哈哈,是不是很爽,sdb 可以 U 盘,也可以是普通硬盘5. 查看某个进程的运行时间可能,大部分同学只会使用 ps aux,其实可以通过 -o 参数,指定只显示具体的某个字段,会得到更清晰的结果。$ ps -p 10167 -o etimes,etime ELAPSED ELAPSED 1712055 19-19:34:15通过 etime 获取该进程的运行时间,可以很直观地看到,进程运行了 19 天同样,可以通过 -o 指定 rss 可以只获取该进程的内存信息。$ ps -p 10167 -o rss RSS 21806. 动态实时查看日志通过 tail 命令 -f 选项,可以动态地监控日志文件的变化,非常实用$ tail -f test.log如果想在日志中出现 Failed 等信息时立刻停止 tail 监控,可以通过如下命令来实现:$ tail -f test.log | sed '/Failed/ q'7. 时间戳的快速转换时间操作,对程序员来说就是家常便饭。有时候希望能够将时间戳,转换为日期时间,在 Linux 命令行上,也可以快速的进行转换:$ date -d@1234567890 +"%Y-%m-%d %H:%M:%S" 2009-02-14 07:31:30当然,也可以在命令行上,查看当前的时间戳$ date +%s 16175141418. 优雅的计算程序运行时间在 Linux 下,可以通过 time 命令,很容易获取程序的运行时间:$ time ./test real 0m1.003s user 0m0.000s sys 0m0.000s可以看到,程序的运行时间为: 1.003s。细心的同学,会看到 real 貌似不等于 user + sys,而且还远远大于,这是怎么回事呢?先来解释下这三个参数的含义:real:表示的钟表时间,也就是从程序执行到结束花费的时间;user:表示运行期间,cpu 在用户空间所消耗的时间;sys:表示运行期间,cpu 在内核空间所消耗的时间;由于 user 和 sys 只统计 cpu 消耗的时间,程序运行期间会调用 sleep 发生阻塞,也可能会等待网络或磁盘 IO,都会消耗大量时间。因此对于类似情况,real 的值就会大于其它两项之和。另外,也会遇到 real 远远小于 user + sys 的场景,这是什么鬼情况?这个更好理解,如果程序在多个 cpu 上并行,那么 user 和 sys 统计时间是多个 cpu 时间,实际消耗时间 real 很可能就比其它两个之和要小了9. 命令行查看ascii码我们在开发过程中,通常需要查看 ascii 码,通过 Linux 命令行就可以轻松查看,而不用去 Google 或 Baidu$ man ascii10. 优雅的删除乱码的文件在 Linux 系统中,会经常碰到名称乱码的文件。想要删除它,却无法通过键盘输入名字,有时候复制粘贴乱码名称,终端可能识别不了,该怎么办?不用担心,下边来展示下 find 是如何优雅的解决问题的。$ ls -i 138957 a.txt 138959 T.txt 132395 ڹ��.txt $ find . -inum 132395 -exec rm {} \;命令中,-inum 指定的是文件的 inode 号,它是系统中每个文件对应的唯一编号,find 通过编号找到后,执行删除操作。11. Linux上获取你的公网IP地址在办公或家庭环境,我们的虚拟机或服务器上配置的通常是内网 IP 地址,我们如何知道,在与外网通信时,我们的公网出口 IP 是神马呢?这个在 Linux 上非常简单,一条命令搞定$ curl ip.sb $ curl ifconfig.me上述两条命令都可以12. 如何批量下载网页资源有时,同事会通过网页的形式分享文件下载链接,在 Linux 系统,通过 wget 命令可以轻松下载,而不用写脚本或爬虫$ wget -r -nd -np --accept=pdf http://fast.dpdk.org/doc/pdf-guides/ # --accept:选项指定资源类型格式 pdf13. 历史命令使用技巧分享几个历史命令的使用技巧,能够提高你的工作效率。!!:重复执行上条命令;!N:重复执行 history 历史中第 N 条命令,N 可以通过 history 查看;!pw:重复执行最近一次,以 pw开头的历史命令,这个非常有用,小编使用非常高频;!$:表示最近一次命令的最后一个参数;猜测大部分同学没用过 !$,这里简单举个例子,让你感受一下它的高效用法$ vim /root/sniffer/src/main.c $ mv !$ !$.bak # 相当于 $ mv /root/sniffer/src/main.c /root/sniffer/src/main.c.bak当前工作目录是 root,想把 main.c 改为 main.c.bak。正常情况你可能需要敲 2 遍包含 main.c 的长参数,当然你也可能会选择直接复制粘贴。而我通过使用 !$ 变量,可以很轻松优雅的实现改名,是不是很 hacker 呢?14. 快速搜索历史命令在 Linux 下经常会敲很多的命令,我们要怎么快速查找并执行历史命令呢?通过上下键来翻看历史命令,No No No,可以通过执行 Ctrl + r,然后键入要所搜索的命令关键词,进行搜索,回车就可以执行,非常高效。15. 真正的黑客不能忽略技巧最后,再分享一个真正的黑客不能忽略技巧。我们在所要执行的命令前,加一个空格,那这条命令就不会被 history 保存到历史记录有时候,执行的命令中包含敏感信息,这个小技巧就显得非常实用了,你也不会再因为忘记执行 history -c 而烦恼了。
2021年04月07日
96 阅读
0 评论
1 点赞
2021-03-21
一文彻底搞懂 zookeeper 核心知识点
初识 zookeeperZookeeper 它作为Hadoop项目中的一个开源子项目,是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。1、zookeeper数据模型zookeeper 维护了一个类似文件系统的数据结构,每个子目录(/微信、/微信/公众号)都被称作为 znode 即节点。和文件系统一样,我们可以很轻松的对 znode 节点进行增加、删除等操作,而且还可以在一个znode下增加、删除子znode,区别在于文件系统的是,znode可以存储数据(严格说是必须存放数据,默认是个空字符)。由于zookeeper是目录节点结构,在获取和创建节点时,必须要以“/” 开头,否则在获取节点时会报错 Path must start with / character。[zk: localhost:2181(CONNECTED) 13] get test Command failed: java.lang.IllegalArgumentException: Path must start with / character根节点名必须为“/XXX”,创建子节点时必须要带上根节点目录“/XXX/CCC”、“/XXX/AAA”。例如:想要获取下图 程序员内点事 节点必须拼接完整的路径 get /微信/公众号/程序员内点事get /微信/公众号/程序员内点事在这里插入图片描述znode被用来存储 byte级 或 kb级 的数据,可存储的最大数据量是1MB(请注意:一个节点的数据量不仅包含它自身存储数据,它的所有子节点的名字也要折算成Byte数计入,因此znode的子节点数也不是无限的)虽然可以手动的修改节点存储量大小,但一般情况下并不推荐这样做。2、znode节点属性一个znode节点不仅可以存储数据,还有一些其他特别的属性。接下来我们创建一个/test节点分析一下它各个属性的含义。[zk: localhost:2181(CONNECTED) 6] get /test 456 cZxid = 0x59ac // ctime = Mon Mar 30 15:20:08 CST 2020 mZxid = 0x59ad mtime = Mon Mar 30 15:22:25 CST 2020 pZxid = 0x59ac cversion = 0 dataVersion = 2 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 3 numChildren = 0 节点属性注解cZxid该数据节点被创建时的事务IdmZxid该数据节点被修改时最新的事物IdpZxid当前节点的父级节点事务Idctime该数据节点创建时间mtime该数据节点最后修改时间dataVersion当前节点版本号(每修改一次值+1递增)cversion子节点版本号(子节点修改次数,每修改一次值+1递增)aclVersion当前节点acl版本号(节点被修改acl权限,每修改一次值+1递增)ephemeralOwner临时节点标示,当前节点如果是临时节点,则存储的创建者的会话id(sessionId),如果不是,那么值=0dataLength当前节点所存储的数据长度numChildren当前节点下子节点的个数我们看到一个znode节点的属性比较多,但比较主要的属性还是zxid、version、acl 这三个。Zxid:znode节点状态改变会导致该节点收到一个zxid格式的时间戳,这个时间戳是全局有序的,znode节点的建立或者更新都会产生一个新的。如果zxid1的值 < zxid2的值,那么说明zxid2发生的改变在zxid1之后。每个znode节点都有3个zxid属性,cZxid(节点创建时间)、mZxid(该节点修改时间,与子节点无关)、pZxid(该节点或者该节点的子节点的最后一次创建或者修改时间,孙子节点无关)。zxid属性主要应用于zookeeper的集群,这个后边介绍集群时详细说。Version:znode属性中一共有三个版本号dataversion(数据版本号)、cversion(子节点版本号)、aclversion(节点所拥有的ACL权限版本号)。znode中的数据可以有多个版本,如果某一个节点下存有多个数据版本,那么查询这个节点数据就需要带上版本号。每当我们对znode节点数据修改后,该节点的dataversion版本号会递增。当客户端请求该znode节点时,会同时返回节点数据和版本号。另外当dataversion为 -1的时候可以忽略版本进行操作。对一个节点设置权限时aclVersion版本号会递增,下边会详细说ACL权限控制。验证一下,我们修改/test节点的数据看看dataVersion有什么变化,发现dataVersion属性变成了 3,版本号递增了。[zk: localhost:2181(CONNECTED) 10] set /test 8888 cZxid = 0x59ac ctime = Mon Mar 30 15:20:08 CST 2020 mZxid = 0x59b6 mtime = Mon Mar 30 16:58:08 CST 2020 pZxid = 0x59ac cversion = 0 dataVersion = 3 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 4 numChildren = 03、znode的类型zookeeper 有四种类型的znode,在用客户端 client 创建节点的时候需要指定类型。zookeeper.create("/公众号/程序员内点事", "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);PERSISTENT-持久化目录节点 :client创建节点后,与zookeeper断开连接该节点将被持久化,当client再次连接后节点依旧存在。PERSISTENT_SEQUENTIAL-持久化顺序节点 :client创建节点后,与zookeeper断开连接该节点将被持久化,再次连接节点还存在,zookeeper会给该节点名称进行顺序编号,例如:/lock/0000000001、/lock/0000000002、/lock/0000000003。EPHEMERAL-临时目录节点 :client与zookeeper断开连接后,该节点即会被删除EPHEMERAL_SEQUENTIAL-临时顺序节点 :client与zookeeper断开连接后,该节点被删除,会给该节点名称进行顺序编号,例如:/lock/0000000001、/lock/0000000002、/lock/0000000003。节点的ACL权限控制ACL:即 Access Control List (节点的权限控制),通过ACL机制来解决znode节点的访问权限问题,要注意的是zookeeper对权限的控制是基于znode级别的,也就说节点之间的权限不具有继承性,即子节点不继承父节点的权限。zookeeper中设置ACL权限的格式由<schema>:<id>:<acl>三段组成。schema :表示授权的方式world:表示任何人都可以访问auth:只有认证的用户可以访问digest:使用username :password用户密码生成MD5哈希值作为认证IDhost/ip:使用客户端主机IP地址来进行认证id:权限的作用域,用来标识身份,依赖于schema选择哪种方式。acl:给一个节点赋予哪些权限,节点的权限有create,、delete、write、read、admin 统称 cdwra。1、world:表示任何人都可以访问我们用 getAcl 命令来看一下,没有设置过权限的znode节点,默认情况下的权限情况。[zk: localhost:2181(CONNECTED) 12] getAcl /test 'world,'anyone : cdrwa看到没有设置ACL属性的节点,默认schema 使用的是world,作用域是anyone,节点权限是cdwra,也就是说任何人都可以访问。那我们如果要给一个schema 为非world的节点设置world权限咋搞?setAcl /test world:anyone:crdwa2、auth:只有认证的用户可以访问schema 用auth授权表示只有认证后的用户才可以访问,那么首先就需要添加认证用户,添加完以后需要对认证的用户设置ACL权限。addauth digest test:password(明文)需要注意的是设置认证用户时的密码是明文的。[zk: localhost:2181(CONNECTED) 2] addauth digest user:user //用户名:密码 [zk: localhost:2181(CONNECTED) 5] setAcl /test auth:user:crdwa [zk: localhost:2181(CONNECTED) 6] getAcl /test 'digest,'user:ben+k/3JomjGj4mfd4fYsfM6p0A= : cdrwa实际上我们这样设置以后,就是将这个节点开放给所有认证的用户,setAcl /test auth:user:crdwa 相当于setAcl /test auth::crdwa。3、digest:用户名:密码的验证方式用户名:密码方式授权是针对单个特定用户,这种方式是不需要先添加认证用户的。如果在代码中使用zookeeper客户端设置ACL,那么密码是明文的,但若是zk.cli等客户端操作就需要将密码进行sha1及base64处理。setAcl <path> digest:<user>:<password(密文)>:<acl> setAcl /test digest:user:jalRr+knv/6L2uXdenC93dEDNuE=:crdwa那么密码如何加密嘞?有以下几种方式:通过shell命令加密echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64使用zookeeper自带的类库org.apache.zookeeper.server.auth.DigestAuthenticationProvider生成java -cp /zookeeper-3.4.13/zookeeper-3.4.13.jar:/zookeeper-3.4.13/lib/slf4j-api-1.7.25.jar \ org.apache.zookeeper.server.auth.DigestAuthenticationProvider \ root:root root:root->root:qiTlqPLK7XM2ht3HMn02qRpkKIE=4、host/ip:使用客户端主机IP地址来进行认证这种方式就比较好理解了,通过对特定的IP地址,也可以是一个IP段进行授权。[zk: localhost:2181(CONNECTED) 3] setAcl /test0000000014 ip:127.0.0.1:crdwa cZxid = 0x59ac ctime = Mon Mar 30 15:20:08 CST 2020 mZxid = 0x59b6 mtime = Mon Mar 30 16:58:08 CST 2020 pZxid = 0x59ac cversion = 0 dataVersion = 3 aclVersion = 3 // 这个版本一直在增加 ephemeralOwner = 0x0 dataLength = 4 numChildren = 0zookeeper 灵魂 watcher我们在开头就说过:zookeeper可以为dubbo提供服务的注册与发现,作为注册中心,但你有想过zookeeper为啥能够实现服务的注册与发现吗?这就不得不说一下zookeeper的灵魂 Watcher(监听者)。1、watcher是个啥?watcher 是zooKeeper中一个非常核心功能 ,客户端watcher 可以监控节点的数据变化以及它子节点的变化,一旦这些状态发生变化,zooKeeper服务端就会通知所有在这个节点上设置过watcher的客户端 ,从而每个客户端都很快感知,它所监听的节点状态发生变化,而做出对应的逻辑处理。简单的介绍了一下watcher ,那么我们来分析一下,zookeeper是如何实现服务的注册与发现。zookeeper的服务注册与发现,主要应用的是zookeeper的znode节点数据模型和watcher机制,大致的流程如下:在这里插入图片描述服务注册: 服务提供者(Provider)启动时,会向zookeeper服务端注册服务信息,也就是创建一个节点,例如:用户注册服务com.xxx.user.register,并在节点上存储服务的相关数据(如服务提供者的ip地址、端口等)。服务发现: 服务消费者(Consumer)启动时,根据自身配置的依赖服务信息,向zookeeper服务端获取注册的服务信息并设置watch监听,获取到注册的服务信息之后,将服务提供者的信息缓存在本地,并进行服务的调用。服务通知: 一旦服务提供者因某种原因宕机不再提供服务之后,客户端与zookeeper服务端断开连接,zookeeper服务端上服务提供者对应服务节点会被删除(例如:用户注册服务com.xxx.user.register),随后zookeeper服务端会异步向所有消费用户注册服务com.xxx.user.register,且设置了watch监听的服务消费者发出节点被删除的通知,消费者根据收到的通知拉取最新服务列表,更新本地缓存的服务列表。上边的过程就是zookeeper可以实现服务注册与发现的大致原理。2、watcher类型znode节点可以设置两类watch,一种是DataWatches,基于znode节点的数据变更从而触发 watch 事件,触发条件getData()、exists()、setData()、 create()。另一种是Child Watches,基于znode的孩子节点发生变更触发的watch事件,触发条件 getChildren()、 create()。而在调用 delete() 方法删除znode时,则会同时触发Data Watches和Child Watches,如果被删除的节点还有父节点,则父节点会触发一个Child Watches。3、watcher特性watch对节点的监听事件是一次性的!客户端在指定的节点设置了监听watch,一旦该节点数据发生变更通知一次客户端后,客户端对该节点的监听事件就失效了。如果还要继续监听这个节点,就需要我们在客户端的监听回调中,再次对节点的监听watch事件设置为True。否则客户端只能接收到一次该节点的变更通知。zookeeper 实现的功能服务的注册与发现功能只是zookeeper的冰山一角,它还能实现诸如分布式锁、队列、配置中心等一系列功能,接下来我们只分析一下原理,具体的实现大家上网查一下资料还是比较全的。1、分布式锁zookeeper基于watcher机制和znode的有序节点,天生就是一个分布式锁的坯子。首先创建一个/test/lock父节点作为一把锁,尽量是持久节点(PERSISTENT类型),每个尝试获取这把锁的客户端,在/test/lock父节点下创建临时顺序子节点。由于序号的递增性,我们规定序号最小的节点即获得锁。例如:客户端来获取锁,在/test/lock节点下创建节点为/test/lock/seq-00000001,它是最小的所以它优先拿到了锁,其它节点等待通知再次获取锁。/test/lock/seq-00000001执行完自己的逻辑后删除节点释放锁。那么节点/test/lock/seq-00000002想要获取锁等谁的通知呢?这里我们让/test/lock/seq-00000002节点监听/test/lock/seq-00000001节点,一旦/test/lock/seq-00000001节点删除,则通知/test/lock/seq-00000002节点,让它再次判断自己是不是最小的节点,是则拿到锁,不是继续等通知。以此类推/test/lock/seq-00000003节点监听/test/lock/seq-00000002节点,总是让后一个节点监听前一个节点,不用让所有节点都监听最小的节点,避免设置不必要的监听,以免造成大量无效的通知,形成“羊群效应”。zookeeper分布式锁和redis分布式锁相比,因为大量的创建、删除节点性能上比较差,并不是很推荐。2、分布式队列zookeeper实现分布式队列也很简单,应用znode的有序节点天然的“先进先出”,后创建的节点总是最大的,出队总是拿序号最小的节点即可。3、配置管理现在有很多开源项目都在使用Zookeeper来维护配置,像消息队列Kafka中,就使用Zookeeper来维护broker的信息;dubbo中管理服务的配置信息。原理也是基于watcher机制,例如:创建一个/config节点存放一些配置,客户端监听这个节点,一点修改/config节点的配置信息,通知各个客户端数据变更重新拉取配置信息。4、命名服务zookeeper的命名服务:也就是我们常说的服务注册与发现,主要是根据指定名字来获取资源或服务的地址,服务提供者等信息,利用其znode节点的特点和watcher机制,将其作为动态注册和获取服务信息的配置中心,统一管理服务名称和其对应的服务器列表信息,我们能够近乎实时地感知到后端服务器的状态(上线、下线、宕机)。
2021年03月21日
89 阅读
0 评论
0 点赞
2021-03-13
MongoDB:开发人员实践指北
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。它介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。1. 左右为难:MySQL vs MongoDB1.1 对比一览指标MySQLMongoDB开发语言C++,CC++,C数据库类型SQLNoSQL(面向文档)SchemaStrictDynamic可扩展性垂直扩展水平扩展核心特性全文搜索索引 支持集成复制 触发器 子查询 查询缓存 支持多存储引擎自动分片 本机复制 内存加速 支持嵌套数据模型 辅助索引(Secondary Index) 富查询 多存储引擎应用场景表行数据结构 强依赖多行事务 经常更新或修改数据记录 关系型数据集高写负载 不稳定的Schema 数据库预计很大 位置数据 不可靠环境的高可用要求 没有DBA优势成熟 兼容性 可复制 分片动态模式 可扩展性 以管理 速度 灵活性1.2 从使用看对比1.2.1 文档数据类型:字段结构更丰富MongoDB的数据类型非常丰富,BSON/JSON结构,可以表示任意结构的数据。MySQL的数据类型相对贫瘠,行列结构,层级结构数据需要拆表、联立表示,复杂了数据管理方式。除此之外,业务数据与DB数据需要ORM来完成映射表示,代码复杂度较高。1.2.2 Schema:让数据更灵活MongoDB是动态Schema,无需预先定义Collection结构;而MySQL是静态Schema,需要预先定义Table结构。举例来说,如果我们的数据时效性以一个月为限,很少会追溯过期数据,我们更希望将表以时间为划分依据,如Data-201901、Data-201902等,这在MongoDB中可以轻易实现,而如果在MySQL中则要复杂许多,要么动态执行Create Table语句,要么将数据放在一起。在之前写过的Slowlog管理系统中,因为数据量较大,采用MySQL存储数据时,需要定时任务去定期归档数据,代码复杂度搞。MongoDB是NoStrict Schema,同一Collection的文档结构允许不同;而MySQL是Strict Schema,同一Table的字段结构必须相同。因此,为了实现复杂的数据结构,MySQL常常会将一个业务结构拆成多张表表示,也会通过空字段处理,即有的记录的字段有值,有的是空值。非强制性Schema最大的好处就是支持业务变化、代码重构,尤其是业务不稳定阶段,表(集合)结构经常变化,但又害怕数据丢失,因此只能不断加字段,导致一张表中会有很多无效字段,而在非强制Schema中,每条记录(文档)的结构都可以不一样,因此新变化对老数据无侵害。1.2.3 事务管理:MongoDB的短板事务管理是MongoDB的软肋,目前v4.2版本已经推出了事务功能,但事务操作必须针对已存在的集合(Existing Collection)。2. MongoDB常见操作2.1 查看库表show dbs // 列举MongoDB实例的数据库 show collections // 列举当前库下的所有集合2.2 查看表数据MongoDB的查询语法比较丰富,既有普通文档的条件查询,还有嵌入文档、数组字段的查询,具体语法可以参见官网https://docs.mongodb.com/manual/tutorial/query-documents/,MongoDB的文档除了讲解说明外,还有交互终端,用户可以自由使用。无论是何种查询,最终都离不开两个问题:(1)查询条件是什么?(2)查询结果又是什么样子的?2.2.1 查询表达式(Query Selector)如上是MongoDB的一些操作符,通过这些操作符和字段、值的联立组合,可以形成复杂的查询条件。假设,我们要在部署记录集合(deploy_record)中查询用户(sli4)、在12月部署的失败(Failure)和取消(Cancel)的记录:db.deploy_record.find({ user:"sli4", date: { "gte": ISODate("2019-12-01T00:00:00Z), "lt": ISODate("2020-01-01T00:00:00Z) }, status:{$in: ["Failure", "Cancel"]} })很多技术内容,光看文档是无法真正领会的,需要有真实的操作场景,才能碰到自己想不到的问题。本想写一篇概括性、总结文章,可以一页指北,却发现,只能提供概要性说明,具体实践过程,还是要面向文档和Google编程!2.2.2 投射投射,就是SQL中的字段选择。MongoDB默认返回符合查询条件文档的全部字段,为了限制MongoDB返回的数据量,可以通过投射表达式选择需要的字段。db.collection.find({查询条件},{投射表达式})投射表达式,就是为指定字段设定是否返回标识,1标识返回,0标识不返回。如部署记录(deploy_record)只返回应用(app)、状态(status):注意:MongoDB默认是返回主键(_id),因此如果不需要主键时,需要显示指定_id:0。2.2.3 聚合Aggregation operations process data records and return computed results.聚合操作通过数据分组、针对分组数据执行一系列操作、最终生成单一计算结果值。聚合提供Pipeline、MapReduce、Methods三种方式。聚合操作可以简化应用层的业务逻辑,在数据层提取需要的数据值。因为聚合比较重要,也很复杂,我们将在下面另起一个模块讲解说明。2.3 创建数据:insertdb.collection.insertOne() // 插入单一文档 db.collection.insertMany() // 插入多个文档 db.collection.insert() // 插入一个或多个文档MongoDB的数据插入行为有三点内容需要明确:如果插入数据时,集合尚未存在,insert操作会触发集合创建。mongodb中每个文档都是以_id作为主键,如果插入的文档中没有指定_id,mongodb会默认分配唯一ID。无论插入一个还是多个文档,mongodb都是以单一文档为原子性维度,即插入过程中如遇错误,只影响正在插入的文档,不影响已经成功的文档,不会回退至插入操作前的状态。2.4 修改操作// 修改操作的命令格式 { <update operator> : { <field1>:<value1>, ... }, <update operator> : { <field1>:<value1>, ... }, ... }2.4.1 修改还是替换上图的两个操作,一个是替换,一个是字段更新,在没有$set更新操作符时,是以新值替代旧值;而有$set时则只针对指定字段更新。db.deploy_record.update({_id: 1}, {user: "王五"}) db.deploy_record.update({_id: 1}, {$set: {user: "王五"}})2.4.2 更新操作注意事项_id字段不可更改upsert操作:当upsert为true时,如果没有匹配的文档可供修改,会将更新文档作为新文档插入集合。2.4.3 还有更多操作符,具体使用结合文档和场景https://docs.mongodb.com/manual/reference/operator/update/2.5 删除操作db.collection.delete({查询表达式})3. MongoDB的高级操作--聚合3.1 Pipeline3.2 MapReduceMapReduce是大数据处理模型,通过map和reduce两个阶段获取理想的数据结果:Map:通过map函数提取一组kv对,每个key都有多个value,准确的说是key-values。Reduce:通过reduce函数,以key为组,分组计算求得结果。在MongoDB中,MapReduce的命令格式如下:除了提供map、reduce函数外,还需要指定数据的筛选条件(query)和数据结果的保存集合(out){ function map(){}, function reduce(key, values){}, { query: {查询表达式}, out: 输出集合 } }转自:https://my.oschina.net/u/4621641/blog/4517354
2021年03月13日
106 阅读
0 评论
0 点赞
2021-03-01
图神经网络,这到底是个什么?
摘要:图神经网络是一种基于图结构的深度学习方法。1、什么是图神经网络图神经网络(Graph Neu做ral Networks, GNNs)是一种基于图结构的深度学习方法,从其定义中可以看出图神经网络主要由两部分组成,即“图”和“神经网络”。这里的“图”是图论中的图数据结构,“神经网络”是我们熟悉的深度学习NN结构,如MLP,CNN,RNN等。要了解图神经网络我们需要先回顾一下“图”和“神经网络”的基本概念。1.1 图的定义1.2 典型神经网络典型的神经网络结构有两条主线,一条主线是卷积神经网络,简称CNN,主要用于图像类数据的处理。另一条主线是循环神经网络,简称RNN,主要用于时序类数据的处理。由于神经网络结构的介绍不是本篇的重点,因此在这里不做重点介绍。只展示如下两图典型的CNN和RNN的结构:下图展示了当前的主流神经网络结构以及适用的场景:1.3 图神经网络根据上述对图和神经网络的回顾,我们可以看出,图神经网络就是借助神经网络的“能力”如深度特征抽取等来处理图结构的数据,因此对于图神经网络,其直观的结构应该如下图:其中图结构的数据有许多,如社交网络图、交通路线图、人物关系图、分子结构图、计算结网络拓扑图等等。这些数据都可以作为图神经网络的输入。之后经过特定的神经网络结构,如MLP,CNN,RNN等的基于图结构的运算,可以完成对于图表示的分类,图的节点或边的预测等功能。2、为什么需要图神经网络近年来,深度学习已经彻底改变了许多机器学习任务,从图像分类和视频处理,到语音识别和自然语言理解,这些任务中的数据通常表示在欧几里得空间中。然而,在越来越多的应用程序中,数据是从非欧几里得域生成的,并表示为具有复杂关系和对象之间相互依赖的图形。图数据的复杂性给现有的机器学习算法带来了巨大的挑战。下图左为图像(欧几里得空间),右为图(非欧几里得空间)。传统的神经网络结构如CNN、RNN等都是接受欧几里得空间的数据作为输入,他们无法处理非欧几里得空间的数据结构,比如图和流行结构。因此对于此类数据,图神经网络就更加适合处理。近年来图神经网络的研究热度也不断提升,如下图所示:3、图神经网络典型的应用场景本章节基于图神经网络近年来的一些研究进展,展示一下图神经网络当前典型的应用场景以及一些典型的任务。将图结构和节点内容信息作为模型的输入,GNNs的输出可以通过以下机制之一专注于不同的图分析任务:Node-level输出用于点回归和分类任务。Edge-level输出与边分类和链路预测任务相关。Graph-level输出和图分类任务相关,比如图表示。下面以典型论文为例介绍几个GNNs的典型任务:3.1 图分类我们知道很多有机物或者化合物的分子结构都是可以用图结构来表示的,比如下图的4-nitroindole,该GNN的作用是训练一个图神经网络,接收一个分子结构来判断该分子结构会不会导致发生突变。在训练的过程中如果有现存的已标注的可导致发生突变的分子结构,我们就可以训练该图神经网络,然后用他来预测一个新的未知的分子会不会导致突变。3.2 图生成我们知道在图像和语言的领域里分别有embedding和generation技术,比如常见的图像和语言生成技术,比如动态静态的预训练和词嵌入技术。相应的在图领域,我们也有图的嵌入表示比如graph embedding representation和图的generation技术。比如下图的graphvae,变分图自编码器就是一个图生成模型,其主要是为图中节点找寻合适的 Embedding 向量,并通过 Embedding 向量实现图重构。其中获取到的节点 Embedding 可以用于支撑下游任务。比如在新的分子结构生成发现中可以使用该技术来加快分子发现速度。3.3 社交网络分析在社交网络分析中,实体之间的关系往往会是非常重要的特征,图结构就能很好的表示这种关系特征。如下图的社交网络图中,每个实体的关系可以用边来描述,这样在进行实体分类或者关系分类时,利用图数据结构,完成特定任务的标注,就可以训练出一个图神经网络来完成此类任务。3.4 网络拓扑分析网络的拓扑天然就是图结构的表示,计算机网络中的路由技术就是以图轮为基础的算路技术。同时网络中每两个节点之间也会有时延,丢包,抖动等网络KPI信息。这些点对之间的KPI往往是动态变化的,这就影响到了实时路由决策和优化的问题。比如当前链路的时延或者丢包过大,路由算法就需要选择新的路径进行数据包传递。图神经网络在这个问题中就可以接收底层的网络拓扑、网络配置信息和流量矩阵信息来实时预测每一个点对,每一条流的实验丢包抖动,这样就可以更好的配合路由和优化算法,使能网络的自动驾驶。4、图神经网络典型训练框架4.1 Semi-supervised learning for node-level classification:给定一个网络,其中部分节点被标记,其他节点未标记,ConvGNNs可以学习一个鲁棒模型,有效地识别未标记节点的类标签。为此,可以通过叠加一对图卷积层,然后是用于多类分类的softmax层来构建端到端框架。见图(a)4.2 Supervised learning for graph-level classification:图级分类的目的是预测整个图的类标签。该任务的端到端学习可以结合图卷积层、图池层和/或readout层来实现。图卷积层负责精确的高级节点表示,图池层则扮演下采样的角色,每次都将每个图粗化成一个子结构。readout层将每个图的节点表示折叠成一个图表示。通过在图表示中应用一个多层感知器和一个softmax层,我们可以建立一个端到端图分类框架。见图(b)4.3 Unsupervised learning for graph embedding:当图中没有可用的类标签时,我们可以学习在端到端框架中以完全无监督的方式嵌入图。这些算法以两种方式利用边缘级信息。一种简单的方法是采用自编码器框架,编码器使用图卷积层将图嵌入到潜在表示中,在潜在表示上使用解码器重构图结构。另一种常用的方法是利用负采样方法(negative sampling),即对图中有链接的部分节点对进行负采样,而对图中有链接的节点对进行正采样。然后应用逻辑回归层对的正负配对进行区分。见图(c)参考文献[1]. https://mp.weixin.qq.com/s/PSrgm7frsXIobSrlcoCWxw[2]. http://speech.ee.ntu.edu.tw/~tlkagk/courses/ML2020/GNN.pdf[3]. https://persagen.com/files/misc/scarselli2009graph.pdf[4]. https://arxiv.org/pdf/1802.03480.pdf[5]. https://arxiv.org/abs/1901.00596[6]. https://arxiv.org/abs/1910.01508
2021年03月01日
65 阅读
0 评论
0 点赞
2021-02-21
万字长文:解读区块链7类共识算法
摘要:本文将对区块链中常见的七类共识算法进行介绍,希望对读者探索区块链有所帮助。区块链技术起源于比特币,最初是比特币等数字货币的一种底层技术,区块链融合了密码学、组网技术、共识算法、智能合约等多种技术。随着区块链技术的逐渐成熟,其逐渐得到科研机构、政府、金融机构和科技企业的关注。区块链具有匿名、防篡改、可追溯和去中心化等特点。传统的交易需要一个可信任的第三方作为交易中介,与之相比,区块链技术能够实现交易的去中心化,同时还能保证全网数据的一致性,使得点对点交易成为可能。这需要对交易确认规则进行设计,这一规则就是本节将要介绍的共识算法。共识算法作为区块链技术的核心,对区块链安全、效率等方面有着决定性的作用。在区块链的应用过程中,共识算法需要解决两个问题:双花问题[1, 2]和拜占庭将军问题[3]。双花问题是指货币在使用过程中重复使用的问题。传统的货币具有实体唯一性,可以通过防伪手段防止双花问题。当前的电子交易也能通过中心的信任机构来解决双花问题。区块链则是通过分布式的节点共同验证交易来解决双花问题。区块链中,一笔交易需要经过足够数量共识节点的验证,在确认无误下对交易进行记录并同步给网络中所有的共识节点。区块链中进行“双花”攻击完成需要付出足够的代价,通过选择共识算法,可以将这一代价扩展到足够大或者使得这一代价超过双花攻击获得的收益。本文将对区块链中常见的七类共识算法进行介绍,希望对读者探索区块链有所帮助。1. 工作量证明(PoW)中本聪在2009年提出的比特币(Bitcoin)是区块链技术最早的应用,其采用PoW作为共识算法,其核心思想是节点间通过哈希算力的竞争来获取记账权和比特币奖励。PoW中,不同节点根据特定信息竞争计算一个数学问题的解,这个数学问题很难求解,但却容易对结果进行验证,最先解决这个数学问题的节点可以创建下一个区块并获得一定数量的币奖励。中本聪在比特币中采用了HashCash[4]机制设计这一数学问题。本节将以比特币采用的PoW算法为例进行说明,PoW的共识步骤如下:节点收集上一个区块产生后全网待确认的交易,将符合条件的交易记入交易内存池,然后更新并计算内存池中交易的Merkle根的值,并将其写入区块头部;在区块头部填写如表1.1所示的区块版本号、前一区块的哈希值、时间戳、当前目标哈希值和随机数等信息;表1.1 区块头部信息随机数nonce在0到232之间取值,对区块头部信息进行哈希计算,当哈希值小于或等于目标值时,打包并广播该区块,待其他节点验证后完成记账;一定时间内如果无法计算出符合要求的哈希值,则重复步骤2。如果计算过程中有其他节点完成了计算,则从步骤1重新开始。比特币产生区块的平均时间为10分钟,想要维持这一速度,就需要根据当前全网的计算能力对目标值(难度)进行调整[5]。难度是对计算产生符合要求的区块困难程度的描述,在计算同一高度区块时,所有节点的难度都是相同的,这也保证了挖矿的公平性。难度与目标值的关系为:难度值=最大目标值/当前目标值 (1.1)其中最大目标值和当前目标值都是256位长度,最大目标值是难度为1时的目标值,即2224。假设当前难度为,算力为,当前目标值为,发现新区块的平均计算时间为,则根据比特币的设计,每产生2016个区块后(约2周)系统会调整一次当前目标值。节点根据前2016个区块的实际生产时间,由公式(1.4)计算出调整后的难度值,如果实际时间生产小于2周,增大难度值;如果实际时间生产大于2周,则减小难度值。根据最长链原则,在不需要节点同步难度信息的情况下,所有节点在一定时间后会得到相同的难度值。在使用PoW的区块链中,因为网络延迟等原因,当同一高度的两个区块产生的时间接近时,可能会产生分叉。即不同的矿工都计算出了符合要求的某一高度的区块,并得到与其相近节点的确认,全网节点会根据收到区块的时间,在先收到的区块基础上继续挖矿。这种情况下,哪个区块的后续区块先出现,其长度会变得更长,这个区块就被包括进主链,在非主链上挖矿的节点会切换到主链继续挖矿。PoW共识算法以算力作为竞争记账权的基础,以工作量作为安全性的保障,所有矿工都遵循最长链原则。新产生的区块包含前一个区块的哈希值,现存的所有区块的形成了一条链,链的长度与工作量成正比,所有的节点均信任最长的区块链。如果当某一组织掌握了足够的算力,就可以针对比特币网络发起攻击。当攻击者拥有足够的算力时,能够最先计算出最新的区块,从而掌握最长链。此时比特币主链上的区块大部分由其生成,他可以故意拒绝某些交易的确认和进行双花攻击,这会对比特币网络的可信性造成影响,但这一行为同样会给攻击者带来损失。通过求解一维随机游走问题,可以获得恶意节点攻击成功的概率和算力之间的关系:图1.1 攻击者算力与攻击成功概率2. 权益证明(PoS)随着参与比特币挖矿的人越来越多,PoW的许多问题逐渐显现,例如随着算力竞争迅速加剧,获取代币需要消耗的能源大量增加,记账权也逐渐向聚集了大量算力的“矿池”集中[6-9]。为此,研究者尝试采用新的机制取代工作量证明。PoS的概念在最早的比特币项目中曾被提及,但由于稳健性等原因没被使用。PoS最早的应用是点点币(PPCoin),PoS提出了币龄的概念,币龄是持有的代币与持有时间乘积的累加,计算如公式(1.4)所示。利用币龄竞争取代算力竞争,使区块链的证明不再仅仅依靠工作量,有效地解决了PoW的资源浪费问题。其中持有时间为某个币距离最近一次在网络上交易的时间,每个节点持有的币龄越长,则其在网络中权益越多,同时币的持有人还会根据币龄来获得一定的收益。点点币的设计中,没有完全脱离工作量证明,PoS机制的记账权的获得同样需要进行简单的哈希计算:其中proofhash是由权重因子、未消费的产出值和当前时间的模糊和得到的哈希值,同时对每个节点的算力进行了限制,可见币龄与计算的难度成反比。在PoS中,区块链的安全性随着区块链的价值增加而增加,对区块链的攻击需要攻击者积攒大量的币龄,也就是需要对大量数字货币持有足够长的时间,这也大大增加了攻击的难度。与PoW相比,采用PoS的区块链系统可能会面对长程攻击(Long Range Attack)和无利害攻击(Nothing at Stake)。除了点点币,有许多币也使用了PoS,但在记账权的分配上有着不同的方法。例如,未来币(Nxt)和黑币(BlackCion)结合节点所拥有的权益,使用随机算法分配记账权。以太坊也在逐步采用PoS代替PoW。3. 委托权益证明(DPoS)比特币设计之初,希望所有挖矿的参与者使用CPU进行计算,算力与节点匹配,每一个节点都有足够的机会参与到区块链的决策当中。随着技术的发展,使用GPU、FPGA、ASIC等技术的矿机大量出现,算力集中于拥有大量矿机的参与者手中,而普通矿工参与的机会大大减小。采用DPoS的区块链中,每一个节点都可以根据其拥有的股份权益投票选取代表,整个网络中参与竞选并获得选票最多的n个节点获得记账权,按照预先决定的顺序依次生产区块并因此获得一定的奖励。竞选成功的代表节点需要缴纳一定数量的保证金,而且必须保证在线的时间,如果某时刻应该产生区块的节点没有履行职责,他将会被取消代表资格,系统将继续投票选出一个新的代表来取代他。DPoS中的所有节点都可以自主选择投票的对象,选举产生的代表按顺序记账,与PoW及PoS相比节省了计算资源,而且共识节点只有确定的有限个,效率也得到了提升。而且每个参与节点都拥有投票的权利,当网络中的节点足够多时,DPoS的安全性和去中心化也得到了保证。4. 实用拜占庭容错算法(PBFT)在PBFT算法中,所有节点都在相同的配置下运行,且有一个主节点,其他节点作为备份节点。主节点负责对客户端的请求进行排序,按顺序发送给备份节点。存在视图(View)的概念,在每个视图中,所有节点正常按照处理消息。但当备份节点检查到主节点出现异常,就会触发视图变换(View Change)机制更换下一编号的节点为主节点,进入新的视图。PBFT中客户端发出请求到收到答复的主要流程如图4.1所示[10] [11],服务器之间交换信息3次,整个过程包含以下五个阶段:图4.1 PBFT执行流程目前以PBFT为代表的拜占庭容错算法被许多区块链项目所使用。在联盟链中,PBFT算法最早是被Hyper ledger Fabric项目采用。Hyperledger Fabric在0.6版本中采用了PBFT共识算法,授权和背书的功能集成到了共识节点之中,所有节点都是共识节点,这样的设计导致了节点的负担过于沉重,对TPS和扩展性有很大的影响。1.0之后的版本都对节点的功能进行了分离,节点分成了三个背书节点(Endorser)、排序节点(Orderer)和出块节点(Committer),对节点的功能进行了分离,一定程度上提高了共识的效率。Cosmos项目使用的Tendermint[12]算法结合了PBFT和PoS算法,通过代币抵押的方式选出部分共识节点进行BFT的共识,其减弱了异步假设并在PBFT的基础上融入了锁的概念,在部分同步的网络中共识节点能够通过两阶段通信达成共识。系统能够容忍1/3的故障节点,且不会产生分叉。在Tendermint的基础上,Hotstuff[13]将区块链的块链式结构和BFT的每一阶段融合,每阶段节点间对前一区块签名确认与新区块的构建同时进行,使算法在实现上更为简单,Hotstuff还使用了门限签名[14]降低算法的消息复杂度。5. Paxos与Raft共识算法是为了保障所存储信息的准确性与一致性而设计的一套机制。在传统的分布式系统中,最常使用的共识算法是基于Paxos的算法。在拜占庭将军问题[3]提出后,Lamport在1990年提出了Paxos算法用于解决特定条件下的系统一致性问题,Lamport于1998年重新整理并发表Paxos的论文[15]并于2001对Paxos进行了重新简述[16]。随后Paxos在一致性算法领域占据统治地位并被许多公司所采用,例如腾讯的Phxpaxos、阿里巴巴的X-Paxos、亚马逊的AWS的DynamoDB和谷歌MegaStore[17]等。这一类算法能够在节点数量有限且相对可信任的情况下,快速完成分布式系统的数据同步,同时能够容忍宕机错误(Crash Fault)。即在传统分布式系统不需要考虑参与节点恶意篡改数据等行为,只需要能够容忍部分节点发生宕机错误即可。但Paxos算法过于理论化,在理解和工程实现上都有着很大的难度。Ongaro等人在2013年发表论文提出Raft算法[18],Raft与Paxos同样的效果并且更便于工程实现。Raft中领导者占据绝对主导地位,必须保证服务器节点的绝对安全性,领导者一旦被恶意控制将造成巨大损失。而且交易量受到节点最大吞吐量的限制。目前许多联盟链在不考虑拜占庭容错的情况下,会使用Raft算法来提高共识效率。6. 结合VRF的共识算法在现有联盟链共识算法中,如果参与共识的节点数量增加,节点间的通信也会增加,系统的性能也会受到影响。如果从众多候选节点中选取部分节点组成共识组进行共识,减少共识节点的数量,则可以提高系统的性能。但这会降低安全性,而且候选节点中恶意节点的比例越高,选出来的共识组无法正常运行的概率也越高。为了实现从候选节点选出能够正常运行的共识组,并保证系统的高可用性,一方面需要设计合适的随机选举算法,保证选择的随机性,防止恶意节点对系统的攻击。另一方面需要提高候选节点中的诚实节点的比例,增加诚实节点被选进共识组的概率。当前在公有链往往基于PoS类算法,抵押代币增加共识节点的准入门槛,通过经济学博弈增加恶意节点的作恶成本,然后再在部分通过筛选的节点中通过随机选举算法,从符合条件的候选节点中随机选举部分节点进行共识。Dodis等人于1999年提出了可验证随机函数(Verifiable Random Functions,VRF)[19]。可验证随机函数是零知识证明的一种应用,即在公私钥体系中,持有私钥的人可以使用私钥和一条已知信息按照特定的规则生成一个随机数,在不泄露私钥的前提下,持有私钥的人能够向其他人证明随机数生成的正确性。VRF可以使用RSA或者椭圆曲线构建,Dodis等人在2002年又提出了基于Diffie-Hellman 困难性问题的可验证随机函数构造方法[20],目前可验证随机函数在密钥传输领域和区块链领域都有了应用[21]。可验证随机函数的具体流程如下:在公有链中,VRF已经在一些项目中得到应用,其中VRF多与PoS算法结合,所有想要参与共识的节点质押一定的代币成为候选节点,然后通过VRF从众多候选节点中随机选出部分共识节点。Zilliqa网络的新节点都必须先执行PoW,网络中的现有节点验证新节点的PoW并授权其加入网络。区块链项目Ontology设计的共识算法VBFT将VRF、PoS和BFT算法相结合,通过VRF在众多候选节点中随机选出共识节点并确定共识节点的排列顺序,可以降低恶意分叉对区块链系统的影响,保障了算法的公平性和随机性。图灵奖获得者Micali等人提出的Algorand[22]将PoS和VRF结合,节点可以采用代币质押的方式成为候选节点,然后通过非交互式的VRF算法选择部分节点组成共识委员会,然后由这部分节点执行类似PBFT共识算法,负责交易的快速验证,Algorand可以在节点为诚实节点的情况下保证系统正常运行。Kiayias等人提出的Ouroboros[23]在第二个版本Praos[24]引入了VRF代替伪随机数,进行分片中主节点的选择。以Algorand等算法使用的VRF算法为例,主要的流程如下:公有链中设计使用的VRF中,节点被选为记账节点的概率往往和其持有的代币正相关。公有链的共识节点范围是无法预先确定的,所有满足代币持有条件的节点都可能成为共识节点,系统需要在数量和参与度都随机的节点中选择部分节点进行共识。而与公有链相比,联盟链参与共识的节点数量有限、节点已知,这种情况下联盟链节点之间可以通过已知的节点列表进行交互,这能有效防止公有链VRF设计时可能遇到的女巫攻击问题。7. 结合分片技术的公式算法分片技术是数据库中的一种技术,是将数据库中的数据切成多个部分,然后分别存储在多个服务器中。通过数据的分布式存储,提高服务器的搜索性能。区块链中,分片技术是将交易分配到多个由节点子集组成的共识组中进行确认,最后再将所有结果汇总确认的机制。分片技术在区块链中已经有一些应用,许多区块链设计了自己的分片方案。Luu等人于2017年提出了Elastico协议,最先将分片技术应用于区块链中[25]。Elastico首先通过PoW算法竞争成为网络中的记账节点。然后按照预先确定的规则,这些节点被分配到不同的分片委员会中。每个分片委员会内部执行PBFT等传统拜占庭容错的共识算法,打包生成交易集合。在超过的节点对该交易集合进行了签名之后,交易集合被提交给共识委员会,共识委员会在验证签名后,最终将所有的交易集合打包成区块并记录在区块链上。Elastico验证了分片技术在区块链中的可用性。在一定规模内,分片技术可以近乎线性地拓展吞吐量。但Elastico使用了PoW用于选举共识节点,这也导致随机数产生过程及PoW竞争共识节点的时间过长,使得交易延迟很高。而且每个分片内部采用的PBFT算法通讯复杂度较高。当单个分片中节点数量较多时,延迟也很高。在Elastico的基础上,Kokoris-Kogias等人提出OmniLedger[26],用加密抽签协议替代了PoW选择验证者分组,然后通过RandHound协议[27]将验证者归入不同分片。OmniLedger。OmniLedger在分片中仍然采用基于PBFT的共识算法作为分片中的共识算法[28],并引入了Atomix协议处理跨分片的交易,共识过程中节点之间通信复杂度较高。当分片中节点数量增多、跨分片交易增多时,系统TPS会显著下降。Wang等人在2019年提出了Monoxide[29]。在PoW区块链系统中引入了分片技术,提出了连弩挖矿算法(Chu ko-nu mining algorithm),解决了分片造成的算力分散分散问题,使得每个矿工可以同时在不同的分片进行分片,在不降低安全性的情况下提高了PoW的TPS。8. 小结本文对区块链中的共识算法进行了综述性介绍,其中对公有链中基础的共识PoW和联盟链中基础的公式算法PBFT进行了较为详细的分析,然后对目前新出现的较为先进的共识算法进行了介绍,希望对读者探索区块链领域有所帮助。参考文献[1]Antonopoulos A M. Mastering Bitcoin: Unlocking Digital Crypto-Currencies[J]. Oreilly Media Inc Usa, 2015.[2]Karame G O, Androulaki E, Capkun S. Two Bitcoins at the Price of One? Double-Spending Attacks on Fast Payments in Bitcoin.[J]. 2012.[3]Lamport L, Shostak R, Pease M. The Byzantine Generals Problem[J]. Acm Transactions on Programming Languages & Systems, 1982,4(3):382-401.[4]Back A. Hashcash - A Denial of Service Counter-Measure: USENIX Technical Conference, 2002[C].[5]Kraft D. Difficulty control for blockchain-based consensus systems[J]. Peer-to-Peer Networking and Applications, 2016,9(2):397-413.[6]Andolfatto D. The False Analogy Between Gold and Bitcoin[J].[7]Alfidi A. The Serious Disadvantages of Bitcoin[J].[8]Miller A, Juels A, Shi E, et al. Permacoin: Repurposing Bitcoin Work for Data Preservation[J]. 2014:475-490.[9]Stegaroiu C E. The Advantages And Disadvantages Of Bitcoin Payments In The New Economy[J]. Annals Economy, 2018,1.[10]范捷, 易乐天, 舒继武. 拜占庭系统技术研究综述[J]. 软件学报, 2013(06):1346-1360.[11]Castro M, Liskov B. Practical Byzantine fault tolerance: Symposium on Operating Systems Design and Implementation, 1999[C].[12]Buchman E. Tendermint: Byzantine fault tolerance in the age of blockchains[D]., 2016.[13]Yin M, Malkhi D, Reiter M K, et al. Hotstuff: Bft consensus with linearity and responsiveness, 2019[C].[14]Desmedt Y, Frankel Y. Shared generation of authenticators and signatures, 1991[C]. Springer.[15]Lamport L. The part-time parliament[J]. Acm Transactions on Computer Systems, 1998,16(2):133-169.[16]Lamport L. Paxos made simple[J]. ACM Sigact News, 2001,32(4):18-25.[17]Chandra T D, Griesemer R, Redstone J. Paxos made live: An engineering perspective: Proceedings of the Twenty-Sixth Annual ACM Symposium on Principles of Distributed Computing, PODC 2007, 2007[C].[18]Ongaro D, Ousterhout J K. In search of an understandable consensus algorithm., 2014[C].[19]Li W, Andreina S, Bohli J, et al. Securing proof-of-stake blockchain protocols, Oslo, Norway, 2017[C]. Springer Verlag, 2017.[20]Dodis Y. Efficient Construction of (Distributed) Verifiable Random Functions: International Workshop on Theory & Practice in Public Key Cryptography: Public Key Cryptography, 2002[C].[21]Melara M S, Blankstein A, Bonneau J, et al. Coniks: Bringing key transparency to end users, Washington, DC, United states, 2015[C]. USENIX Association, 2015.[22]Gilad Y, Hemo R, Micali S, et al. Algorand: Scaling Byzantine Agreements for Cryptocurrencies, Shanghai, China, 2017[C]. Association for Computing Machinery, Inc, 2018.[23]Kiayias A, Russell A, David B, et al. Ouroboros: A Provably Secure Proof-of-Stake Blockchain Protocol, 2017[C].[24]David B, Gaži P, Kiayias A, et al. Ouroboros Praos: An Adaptively-Secure, Semi-synchronous Proof-of-Stake Blockchain, 2018[C].[25]Luu L, Narayanan V, Zheng C, et al. A secure sharding protocol for open blockchains, Vienna, Austria, 2016[C]. Association for Computing Machinery, 2016.[26]Kokoris-Kogias E, Jovanovic P, Gasser L, et al. OmniLedger: A Secure, Scale-Out, Decentralized Ledger via Sharding, Los Alamitos, CA, USA, 2018[C]. IEEE Computer Society, 2018//.[27]Syta E, Jovanovic P, Kogias E K, et al. Scalable Bias-Resistant Distributed Randomness, Los Alamitos, CA, USA, 2017[C]. IEEE Computer Society, 2017//.[28]Kokoris-Kogias E, Jovanovic P, Gailly N, et al. Enhancing bitcoin security and performance with strong consistency via collective signing, Austin, TX, United states, 2016[C]. USENIX Association, 2016.[29]Wang J, Wang H. Monoxide: Scale out blockchains with asynchronous consensus zones: 16th {USENIX} Symposium on Networked Systems Design and Implementation ({NSDI} 19), 2019[C].
2021年02月21日
91 阅读
0 评论
0 点赞
2021-02-08
美团万亿级 KV 存储架构与实践
前言KV 存储作为美团一项重要的在线存储服务,承载了在线服务每天万亿级的请求量。在 2019 年 QCon 全球软件开发大会(上海站)上,美团高级技术专家齐泽斌分享了《美团点评万亿级 KV 存储架构与实践》,本文系演讲内容的整理,主要分为四个部分:第一部分:讲述了美团 KV 存储的发展历程;第二部分:阐述了内存 KV Squirrel 架构和实践;第三部分:介绍了持久化 KV Cellar 架构和实践;最后:分享了未来的发展规划和业界新趋势。美团点评 KV 存储发展历程美团第一代的分布式 KV 存储如下图左侧的架构所示,相信很多公司都经历过这个阶段。在客户端内做一致性哈希,在后端部署很多的 Memcached 实例,这样就实现了最基本的 KV 存储分布式设计。但这样的设计存在很明显的问题:比如在宕机摘除节点时,会丢数据,缓存空间不够需要扩容,一致性哈希也会丢失一些数据等等,这样会给业务开发带来的很多困扰。随着 Redis 项目的成熟,美团也引入了 Redis 来解决我们上面提到的问题,进而演进出来如上图右侧这样一个架构。大家可以看到,客户端还是一样,采用了一致性哈希算法,服务器端变成了 Redis 组成的主从结构。当任何一个节点宕机,我们可以通过 Redis 哨兵完成 Failover,实现高可用。但有一个问题还是没有解决,如果扩缩容的话,一致性哈希仍然会丢数据,那么这个问题该如何解决呢?这个时候,我们发现有了一个比较成熟的 KV 存储开源项目:阿里 Tair 。2014年,我们引入了 Tair 来满足业务 KV 存储方面的需求。Tair 开源版本的架构主要分成三部分:上图下边是存储节点,存储节点会上报心跳到它的中心节点,中心节点内部有两个配置管理节点,会监控所有的存储节点。当有任何存储节点宕机或者扩容时,它会做集群拓扑的重新构建。当客户端启动时,它会直接从中心节点拉来一个路由表。这个路由表简单来说就是一个集群的数据分布图,客户端根据路由表直接去存储节点读写。针对之前 KV 的扩容丢数据问题,它也有数据迁移机制来保证数据的完整性。但是,我们在使用的过程中,还遇到了一些其他问题,比如中心节点虽然是主备高可用的,但实际上它没有类似于分布式仲裁的机制,所以在网络分割的情况下,它是有可能发生“脑裂”的,这个也给我们的业务造成过比较大的影响。另外,在容灾扩容时,也遇到过数据迁移影响到业务可用性的问题。另外,我们之前用过 Redis ,业务会发现 Redis 的数据结构特别丰富,而 Tair 还不支持这些数据结构。虽然我们用 Tair 解决了一些问题,但是 Tair 也无法完全满足业务需求。毕竟,在美团这样一个业务规模较大和业务复杂度较高的场景下,很难有开源系统能很好地满足我们的需求。最终,我们决定在已应用的开源系统之上进行自研。刚好在2015 年, Redis 官方正式发布了集群版本 Redis Cluster。所以,我们紧跟社区步伐,并结合内部需求做了很多开发工作,演进出了全内存、高吞吐、低延迟的 KV 存储 Squirrel。另外,基于 Tair,我们还加入了很多自研的功能,演进出持久化、大容量、数据高可靠的 KV 存储 Cellar 。因为 Tair 的开源版本已经有四五年没有更新了,所以,Cellar 的迭代完全靠美团自研,而 Redis 社区一直很活跃。总的来说,Squirrel 的迭代是自研和社区并重,自研功能设计上也会尽量与官方架构进行兼容。后面大家可以看到,因为这些不同,Cellar 和 Squirrel 在解决同样的问题时也选取了不同的设计方案。这两个存储其实都是 KV 存储领域不同的解决方案。在实际应用上,如果业务的数据量小,对延迟敏感,我们建议大家用 Squirrel ;如果数据量大,对延迟不是特别敏感,我们建议用成本更低的 Cellar 。目前这两套 KV 存储系统在美团内部每天的调用量均已突破万亿,它们的请求峰值也都突破了每秒亿级。内存 KV Squirrel 架构和实践在开始之前,本文先介绍两个存储系统共通的地方。比如分布式存储的经典问题:数据是如何分布的?这个问题在 KV 存储领域,就是 Key 是怎么分布到存储节点上的。这里 Squirrel 跟 Cellar 是一样的。当我们拿到一个 Key 后,用固定的哈希算法拿到一个哈希值,然后将哈希值对 Slot 数目取模得到一个Slot id,我们两个 KV 现在都是预分片16384个 Slot 。得到 Slot id 之后,再根据路由表就能查到这个 Slot 存储在哪个存储节点上。这个路由表简单来说就是一个 Slot 到存储节点的对照表。接下来讲一下对高可用架构的认知,个人认为高可用可以从宏观和微观两个角度来看。从宏观的角度来看,高可用就是指容灾怎么做。比如说挂掉了一个节点,你该怎么做?一个机房或者说某个地域的一批机房宕机了,你该怎么做?而从微观的角度看,高可用就是怎么能保证端到端的高成功率。我们在做一些运维升级或者扩缩容数据迁移的时候,能否做到业务请求的高可用? 本文也会从宏观和微观两个角度来分享美团做的一些高可用工作。上图就是我们的 Squirrel 架构。中间部分跟 Redis 官方集群是一致的。它有主从的结构, Redis 实例之间通过 Gossip 协议去通信。我们在右边添加了一个集群调度平台,包含调度服务、扩缩容服务和高可用服务等,它会去管理整个集群,把管理结果作为元数据更新到 ZooKeeper。我们的客户端会订阅 ZooKeeper 上的元数据变更,实时获取到集群的拓扑状态,直接在 Redis 集群进行读写操作。Squirrel 节点容灾然后再看一下 Squirrel 容灾怎么做。对于 Redis 集群而言,节点宕机已经有完备的处理机制了。官方提供的方案,任何一个节点从宕机到被标记为 FAIL 摘除,一般需要经过 30 秒。主库的摘除可能会影响数据的完整性,所以,我们需要谨慎一些。但是对于从库呢?我们认为这个过程完全没必要。另一点,我们都知道内存的 KV 存储数据量一般都比较小。对于业务量很大的公司来说,它往往会有很多的集群。如果发生交换机故障,会影响到很多的集群,宕机之后去补副本就会变得非常麻烦。为了解决这两个问题,我们做了 HA 高可用服务。它的架构如下图所示,它会实时监控集群的所有节点。不管是网络抖动,还是发生了宕机(比如说 Redis 2 ),它可以实时更新 ZooKeeper ,告诉 ZooKeeper 去摘除 Redis 2 ,客户端收到消息后,读流量就直接路由到 Redis 3上。如果 Redis 2 只是几十秒的网络抖动,过几十秒之后,如果 HA 节点监控到它恢复后,会把它重新加回。如果过了一段时间,HA 判断它属于一个永久性的宕机,HA 节点会直接从 Kubernetes 集群申请一个新的 Redis 4 容器实例,把它加到集群里。此时,拓扑结构又变成了一主两从的标准结构,HA 节点更新完集群拓扑之后,就会去写 ZooKeeper 通知客户端去更新路由,客户端就能到 Redis 4 这个新从库上进行读操作。通过上述方案,我们把从库的摘除时间从 30 秒降低到了 5 秒。另外,我们通过 HA 自动申请容器实例加入集群的方式,把宕机补副本变成了一个分钟级的自动操作,不需要任何人工的介入。Squirrel 跨地域容灾我们解决了单节点宕机的问题,那么跨地域问题如何解决呢?我们首先来看下跨地域有什么不同。第一,相对于同地域机房间的网络而言,跨地域专线很不稳定;第二,跨地域专线的带宽是非常有限且昂贵的。而集群内的复制没有考虑极端的网络环境。假如我们把主库部署到北京,两个从库部署在上海,同样一份数据要在北上专线传输两次,这样会造成巨大的专线带宽浪费。另外,随着业务的发展和演进,我们也在做单元化部署和异地多活架构。用官方的主从同步,满足不了我们的这些需求。基于此,我们又做了集群间的复制方案。如上图所示,这里画出了北京的主集群以及上海的从集群,我们要做的是通过集群同步服务,把北京主集群的数据同步到上海从集群上。按照流程,首先要向我们的同步调度模块下发“在两个集群间建立同步链路”的任务,同步调度模块会根据主从集群的拓扑结构,把主从集群间的同步任务下发到同步集群,同步集群收到同步任务后会扮成 Redis 的 Slave,通过 Redis 的复制协议,从主集群上的从库拉取数据,包括 RDB以及后续的增量变更。同步机收到数据后会把它转成客户端的写命令,写到上海从集群的主节点里。通过这样的方式,我们把北京主集群的数据同步到了上海的从集群。同样的,我们要做异地多活也很简单,再加一个反向的同步链路,就可以实现集群间的双向同步。接下来我们讲一下如何做好微观角度的高可用,也就是保持端到端的高成功率。对于 Squirrel ,主要讲如下三个影响成功率的问题:数据迁移造成超时抖动。持久化造成超时抖动。热点 Key 请求导致单节点过载。Squirrel 智能迁移对于数据迁移,我们主要遇到三个问题:Redis Cluster 虽然提供了数据迁移能力,但是对于要迁哪些 Slot,Slot 从哪迁到哪,它并不管。做数据迁移的时候,大家都想越快越好,但是迁移速度过快又可能影响业务正常请求。Redis 的 Migrate 命令会阻塞工作线程,尤其在迁移大 Value 的时候会阻塞特别久。为了解决这些问题,我们做了全新的迁移服务。下面我们按照工作流,讲一下它是如何运行的。首先生成迁移任务,这步的核心是“就近原则”,比如说同机房的两个节点做迁移肯定比跨机房的两个节点快。迁移任务生成之后,会把任务下发到一批迁移机上。迁移机迁移的时候,有这样几个特点:第一,会在集群内迁出节点间做并发,比如同时给 Redis 1、Redis 3 下发迁移命令。第二,每个 Migrate 命令会迁移一批 Key。第三,我们会用监控服务去实时采集客户端的成功率、耗时,服务端的负载、QPS 等,之后把这个状态反馈到迁移机上。迁移数据的过程就类似 TCP 慢启动的过程,它会把速度一直往上加,若出现请求成功率下降等情况,它的速度就会降低,最终迁移速度会在动态平衡中稳定下来,这样就达到了最快速的迁移,同时又尽可能小地影响业务的正常请求。接下来,我们看一下大 Value 的迁移,我们实现了一个异步 Migrate 命令,该命令执行时,Redis 的主线程会继续处理其他的正常请求。如果此时有对正在迁移 Key 的写请求过来,Redis 会直接返回错误。这样最大限度保证了业务请求的正常处理,同时又不会阻塞主线程。Squirrel 持久化重构Redis 主从同步时会生成 RDB。生成 RDB 的过程会调用 Fork 产生一个子进程去写数据到硬盘,Fork 虽然有操作系统的 COW 机制,但是当内存用量达到 10 G 或 20 G 时,依然会造成整个进程接近秒级的阻塞。这对在线业务来说几乎是无法接受的。我们也会为数据可靠性要求高的业务去开启 AOF,而开 AOF 就可能因 IO 抖动造成进程阻塞,这也会影响请求成功率。对官方持久化机制的这两个问题,我们的解决方案是重构持久化机制。上图是我们最新版的 Redis 持久化机制,写请求会先写到 DB 里,然后写到内存 Backlog,这跟官方是一样的。同时它会把请求发给异步线程,异步线程负责把变更刷到硬盘的 Backlog 里。当硬盘 Backlog 过多时,我们会主动在业务低峰期做一次 RDB ,然后把 RDB 之前生成的 Backlog 删除。如果这时候我们要做主从同步,去寻找同步点的时候,该怎么办?第一步还是跟官方一样,我们会从内存 Backlog 里找有没有要求的同步点,如果没有,我们会去硬盘 Backlog 找同步点。由于硬盘空间很大,硬盘 Backlog 可以存储特别多的数据,所以很少会出现找不到同步点的情况。如果硬盘 Backlog 也没有,我们就会触发一次类似于全量重传的操作,但这里的全量重传是不需要当场生成 RDB 的,它可以直接用硬盘已存的 RDB 及其之后的硬盘 Backlog 完成全量重传。通过这个设计,我们减少了很多的全量重传。另外,我们通过控制在低峰区生成 RDB ,减少了很多 RDB 造成的抖动。同时,我们也避免了写 AOF 造成的抖动。不过,这个方案因为写 AOF 是完全异步的,所以会比官方的数据可靠性差一些,但我们认为这个代价换来了可用性的提升,这是非常值得的。Squirrel 热点 Key下面看一下 Squirrel 的热点 Key 解决方案。如下图所示,普通主、从是一个正常集群中的节点,热点主、从是游离于正常集群之外的节点。我们看一下它们之间怎么发生联系。当有请求进来读写普通节点时,节点内会同时做请求 Key 的统计。如果某个 Key 达到了一定的访问量或者带宽的占用量,会自动触发流控以限制热点 Key 访问,防止节点被热点请求打满。同时,监控服务会周期性的去所有 Redis 实例上查询统计到的热点 Key。如果有热点,监控服务会把热点 Key 所在 Slot 上报到我们的迁移服务。迁移服务这时会把热点主从节点加入到这个集群中,然后把热点 Slot 迁移到这个热点主从上。因为热点主从上只有热点 Slot 的请求,所以热点 Key的处理能力得到了大幅提升。通过这样的设计,我们可以做到实时的热点监控,并及时通过流控去止损;通过热点迁移,我们能做到自动的热点隔离和快速的容量扩充。持久化 KV Cellar 架构和实践下面看一下持久化 KV Cellar 的架构和实践。下图是我们最新的 Cellar 架构图。跟阿里开源的 Tair 主要有两个架构上的不同。第一个是OB,第二个是 ZooKeeper。我们的 OB 跟 ZooKeeper 的 Observer 是类似的作用,提供 Cellar 中心节点元数据的查询服务。它可以实时与中心节点的 Master 同步最新的路由表,客户端的路由表都是从 OB 去拿。 这样做的好处主要有两点,第一,把大量的业务客户端跟集群的大脑 Master 做了天然的隔离,防止路由表请求影响集群的管理。第二,因为 OB 只供路由表查询,不参与集群的管理,所以它可以进行水平扩展,极大地提升了我们路由表的查询能力。另外,我们引入了 ZooKeeper 做分布式仲裁,解决我刚才提到的 Master、Slave 在网络分割情况下的“脑裂”问题,并且通过把集群的元数据存储到 ZooKeeper,我们保证了元数据的高可靠。Cellar 节点容灾介绍完整体的架构,我们看一下 Cellar 怎么做节点容灾。一个集群节点的宕机一般是临时的,一个节点的网络抖动也是临时的,它们会很快地恢复,并重新加入集群。因为节点的临时离开就把它彻底摘除,并做数据副本补全操作,会消耗大量资源,进而影响到业务请求。所以,我们实现了 Handoff 机制来解决这种节点短时故障带来的影响。如上图所示 ,如果 A 节点宕机了,会触发 Handoff 机制,这时候中心节点会通知客户端 A节点发生了故障,让客户端把分片 1 的请求也打到 B 上。B 节点正常处理完客户端的读写请求之后,还会把本应该写入 A 节点的分片 1&2 数据写入到本地的 Log 中。如果 A 节点宕机后 3~5 分钟,或者网络抖动 30~50 秒之后恢复了,A 节点就会上报心跳到中心节点,中心节点就会通知 B 节点:“ A 节点恢复了,你去把它不在期间的数据传给它。”这时候,B 节点就会把本地存储的 Log 回写到 A 节点上。等到 A 节点拥有了故障期间的全量数据之后,中心节点就会告诉客户端,A 节点已经彻底恢复了,客户端就可以重新把分片 1 的请求打回 A 节点。通过这样的操作,我们可以做到秒级的快速节点摘除,而且节点恢复后加回,只需补齐少量的增量数据。另外如果 A 节点要做升级,中心节点先通过主动 Handoff 把 A 节点流量切到 B 节点,A 升级后再回写增量 Log,然后切回流量加入集群。这样通过主动触发 Handoff 机制,我们就实现了静默升级的功能。Cellar 跨地域容灾下面我介绍一下 Cellar 跨地域容灾是怎么做的。Cellar 跟 Squirrel 面对的跨地域容灾问题是一样的,解决方案同样也是集群间复制。以下图一个北京主集群、上海从集群的跨地域场景为例,比如说客户端的写操作到了北京的主集群 A 节点,A 节点会像正常集群内复制一样,把它复制到 B 和 D 节点上。同时 A 节点还会把数据复制一份到从集群的 H 节点。H 节点处理完集群间复制写入之后,它也会做从集群内的复制,把这个写操作复制到从集群的 I 、K 节点上。通过在主从集群的节点间建立这样一个复制链路,我们完成了集群间的数据复制,并且这个复制保证了最低的跨地域带宽占用。同样的,集群间的两个节点通过配置两个双向复制的链路,就可以达到双向同步异地多活的效果。Cellar 强一致我们做好了节点容灾以及跨地域容灾后,业务又对我们提出了更高要求:强一致存储。我们之前的数据复制是异步的,在做故障摘除时,可能因为故障节点数据还没复制出来,导致数据丢失。但是对于金融支付等场景来说,它们是不容许数据丢失的。面对这个难题,我们该怎么解决?目前业界主流的解决方案是基于 Paxos 或 Raft 协议的强一致复制。我们最终选择了 Raft 协议。主要是因为 Raft 论文是非常详实的,是一篇工程化程度很高的论文。业界也有不少比较成熟的 Raft 开源实现,可以作为我们研发的基础,进而能够缩短研发周期。下图是现在 Cellar 集群 Raft 复制模式下的架构图,中心节点会做 Raft 组的调度,它会决定每一个 Slot 的三副本存在哪些节点上。大家可以看到 Slot 1 在存储节点 1、2、4 上,Slot 2 在存储节点2、3、4上。每个 Slot 组成一个 Raft 组,客户端会去 Raft Leader 上进行读写。由于我们是预分配了 16384 个 Slot,所以,在集群规模很小的时候,我们的存储节点上可能会有数百甚至上千个 Slot 。这时候如果每个 Raft 复制组都有自己的复制线程、 复制请求和 Log等,那么资源消耗会非常大,写入性能会很差。所以我们做了 Multi Raft 实现, Cellar 会把同一个节点上所有的 Raft 复制组写一份 Log,用同一组线程去做复制,不同 Raft 组间的复制包也会按照目标节点做整合,以保证写入性能不会因 Raft 组过多而变差。 Raft 内部其实是有自己的选主机制,它可以控制自己的主节点,如果有任何节点宕机,它可以通过选举机制选出新的主节点。那么,中心节点是不是就不需要管理 Raft 组了吗?不是的。这里讲一个典型的场景,如果一个集群的部分节点经过几轮宕机恢复的过程, Raft Leader 在存储节点之间会变得极其不均。而为了保证数据的强一致,客户端的读写流量又必须发到 Raft Leader,这时候集群的节点流量会很不均衡。所以我们的中心节点还会做 Raft 组的 Leader 调度。比如说 Slot 1 存储在节点 1、2、4,并且节点 1 是 Leader。如果节点 1 挂了,Raft 把节点 2 选成了 Leader。然后节点 1 恢复了并重新加入集群,中心节点这时会让节点 2 把 Leader 还给节点 1 。这样,即便经过一系列宕机和恢复,我们存储节点之间的 Leader 数目仍然能保证是均衡的。接下来,我们看一下 Cellar 如何保证它的端到端高成功率。这里也讲三个影响成功率的问题。Cellar 遇到的数据迁移和热点 Key 问题与 Squirrel 是一样的,但解决方案不一样。这是因为 Cellar 走的是自研路径,不用考虑与官方版本的兼容性,对架构改动更大些。另一个问题是慢请求阻塞服务队列导致大面积超时,这是 Cellar 网络、工作多线程模型设计下会遇到的不同问题。Cellar 智能迁移上图是 Cellar 智能迁移架构图。我们把桶的迁移分成了三个状态。第一个状态就是正常的状态,没有任何迁移。如果这时候要把 Slot 2 从 A 节点迁移到 B节点,A 会给 Slot 2 打一个快照,然后把这个快照全量发到 B 节点上。在迁移数据的时候, B 节点的回包会带回 B 节点的状态。B 的状态包括什么?引擎的压力、网卡流量、队列长度等。A 节点会根据 B 节点的状态调整自己的迁移速度。像 Squirrel 一样,它经过一段时间调整后,迁移速度会达到一个动态平衡,达到最快速的迁移,同时又尽可能小地影响业务的正常请求。当 Slot 2 迁移完后, 会进入图中 Slot 3 的状态。客户端这时可能还没更新路由表,当它请求到了 A 节点,A 节点会发现客户端请求错了节点,但它不会返回错误,它会把请求代理到 B 节点上,然后把 B 的响应包再返回客户端。同时它会告诉客户端,需要更新一下路由表了,此后客户端就能直接访问到 B 节点。这样就解决了客户端路由更新延迟造成的请求错误。Cellar 快慢列队下图上方是一个标准的线程队列模型。网络线程池接收网络流量解析出请求包,然后把请求放到工作队列里,工作线程池会从工作队列取请求来处理,然后把响应包放回网络线程池发出。我们分析线上发生的超时案例时发现,一批超时请求当中往往只有一两个请求是引擎处理慢导致的,大部分请求,只是因为在队列等待过久导致整体响应时间过长而超时了。从线上分析来看,真正的慢请求占超时请求的比例只有 1/20。我们的解法是什么样?很简单,拆线程池、拆队列。我们的网络线程在收到包之后,会根据它的请求特点,是读还是写,快还是慢,分到四个队列里。读写请求比较好区分,但快慢怎么分开?我们会根据请求的 Key 个数、Value大小、数据结构元素数等对请求进行快慢区分。然后用对应的四个工作线程池处理对应队列的请求,就实现了快慢读写请求的隔离。这样如果我有一个读的慢请求,不会影响另外三种请求的正常处理。不过这样也会带来一个问题,我们的线程池从一个变成四个,那线程数是不是变成原来的四倍?其实并不是的,我们某个线程池空闲的时候会去帮助其它的线程池处理请求。所以,我们线程池变成了四个,但是线程总数并没有变。我们线上验证中这样的设计能把服务 TP999 的延迟降低 86%,可大幅降低超时率。Cellar 热点 Key上图是 Cellar 热点 Key 解决方案的架构图。我们可以看到中心节点加了一个职责,多了热点区域管理,它现在不只负责正常的数据副本分布,还要管理热点数据的分布,图示这个集群在节点 C、D 放了热点区域。我们通过读写流程看一下这个方案是怎么运转的。如果客户端有一个写操作到了 A 节点,A 节点处理完成后,会根据实时的热点统计结果判断写入的 Key 是否为热点。如果这个 Key 是一个热点,那么它会在做集群内复制的同时,还会把这个数据复制有热点区域的节点,也就是图中的 C、D 节点。同时,存储节点在返回结果给客户端时,会告诉客户端,这个 Key 是热点,这时客户端内会缓存这个热点 Key。当客户端有这个 Key 的读请求时,它就会直接去热点区域做数据的读取。通过这样的方式,我们可以做到只对热点数据做扩容,不像 Squirrel ,要把整个 Slot 迁出来做扩容。有必要的话,中心节点也可以把热点区域放到集群的所有节点上,所有的热点读请求就能均衡的分到所有节点上。另外,通过这种实时的热点数据复制,我们很好地解决了类似客户端缓存热点 KV 方案造成的一致性问题。发展规划和业界趋势最后,一起来看看我们项目的规划和业界的技术趋势。这部分内容会按照服务、系统、硬件三层来进行阐述。首先在服务层,主要有三点:第一,Redis Gossip 协议优化。大家都知道 Gossip 协议在集群的规模变大之后,消息量会剧增,它的 Failover 时间也会变得越来越长。所以当集群规模达到 TB 级后,集群的可用性会受到很大的影响,所以我们后面会重点在这方面做一些优化。第二,我们已经在 Cellar 存储节点的数据副本间做了 Raft 复制,可以保证数据强一致,后面我们会在 Cellar 的中心点内部也做一个 Raft 复制,这样就不用依赖于 ZooKeeper 做分布式仲裁、元数据存储了,我们的架构也会变得更加简单、可靠。第三,Squirrel 和 Cellar 虽然都是 KV 存储,但是因为它们是基于不同的开源项目研发的,所以 API 和访问协议不同,我们之后会考虑将 Squirrel 和 Cellar 在 SDK 层做整合,虽然后端会有不同的存储集群,但业务侧可以用一套 SDK 进行访问。在系统层面,我们正在调研并去落地一些 Kernel Bypass 技术,像 DPDK、SPDK 这种网络和硬盘的用户态 IO 技术。它可以绕过内核,通过轮询机制访问这些设备,可以极大提升系统的 IO 能力。存储作为 IO 密集型服务,性能会获得大幅的提升。在硬件层面,像支持 RDMA 的智能网卡能大幅降低网络延迟和提升吞吐;还有像 3D XPoint 这样的闪存技术,比如英特尔新发布的 AEP 存储,其访问延迟已经比较接近内存了,以后闪存跟内存之间的界限也会变得越来越模糊;最后,看一下计算型硬件,比如通过在闪存上加 FPGA 卡,把原本应该 CPU 做的工作,像数据压缩、解压等,下沉到卡上执行,这种硬件能在解放 CPU 的同时,也可以降低服务的响应延迟。
2021年02月08日
70 阅读
0 评论
0 点赞
2021-02-05
终于搞懂了Python模块之间的相互引用问题
摘要:详细讲解了相对路径和绝对路径的引用方法。在某次运行过程中出现了如下两个报错:报错1: ModuleNotFoundError: No module named '__main__.src_test1'; '__main__' is not a package 报错2: ImportError: attempted relative import with no known parent package 于是基于这两个报错探究了一下python3中的模块相互引用的问题,下面来逐个解析,请耐心看完。好的,我们先来构造第一个错,测试代码结构如下:|--- test_main.py |--- src |--- __init__.py |--- src_test1.py |--- src_test2.pysrc_test2.py 代码class Test2(object): def foo(self): print('I am foo')src_test1.py 代码,引用Test2模块from .src_test2 import Test2 def fun1(): t2 = Test2() t2.foo() if __name__ == "__main__": fun1()此时运行 src_test1.py 报错“No module named '__main__.src_test1'; '__main__' is not a package”问题原因:主要在于引用src_test2模块的时候,用的是相对路径".",在import语法中翻译成"./",也就是当前目录下,按这样理解也没有问题,那为什么报错呢?从 PEP 328 中,我们找到了关于 the relative imports(相对引用)的介绍通俗一点意思就是,你程序入口运行的那个模块,就默认为主模块,他的name就是‘main’,然后会将本模块import中的点(.)替换成‘__main__’,那么 .src_test2就变成了 __main__.src_test2,所以当然找不到这个模块了。解决方法:因此,建议的做法是在 src同层级目录创建 引用模块 test_main.py(为什么不在src目录下创建,待会下一个报错再讲),并引用src_test1模块,代码如下:from src.src_test1 import fun1 if __name__ == "__main__": fun1()那为什么这样执行就可以了呢,其中原理是什么呢?我是这样理解的(欢迎纠正):test_main执行时,他被当做根目录,因此他引用的src.src_test1 是绝对路径,这样引用到哪都不会错,此时他的name=‘main’,当执行src_test1的时候,注意了此时test1的name是 src.src_test1,那么在test1中使用的是相对路径,查找逻辑是先找到父节点(src目录),再找父节点下面的src_test2,因此可以成功找到,Bingo!辅证:构造一个例子,就可以理解上面的 执行目录就是根目录 的说法了,修改test1,使引用test_main:from .. import test_main 报错:ValueError: attempted relative import beyond top-level packageOK,那继续构造第二个报错:上文中说过,解决main 的问题,就是创建一个模块,来调用使用相对路径的模块,那么为什么我不能在相同目录下创建这个文件来调用呢?让我们来测试下代码:创建test_src.py文件,代码结构变更如下:|--- test_main.py |--- src |--- __init__.py |--- src_test1.py |--- src_test2.pys |--- test_src.pytest_src 代码:from src_test1 import fun1 if __name__ == "__main__": fun1()执行报错:ImportError: attempted relative import with no known parent package问题原因:当执行test_src时,按上文理解,此时执行文件所在的目录为根目录,那么引用test1的时候,需要注意的是,此时test1的name属性不再是src.src_test1,因为程序感知不到src的存在,此时他的绝对路径是 src_test1,此时再次引用相对路径查找的test2,同样的步骤,需要先找到父节点,而此时他自己就是根节点了,已经没有父节点了,因此报错“no known parent package”。解决方法:此时为了避免父节点产生矛盾,因此将test1中的引入去掉相对引用即可from .src_test2 import Test2 --> from src_test2 import Test2继续深入:那使用相对路径和绝对路径,编译器是怎么找到这个模块的呢?执行import的时候,存在一个引入的顺序,即优先查找执行目录下有没有此文件,如没有,再查找lib库下,如还没有,再查找sys.path中的路径,如再没有,报错。所以不管是当前目录,还是 sys.path中的目录,都可以查到 src_test2这个模块,就可以编译成功。号外:解决完上述问题后,不管我们用哪种方式,我们调试代码时,都是单个文件调试,但此时根目录就不对了,import方式又要改动,执行起来很麻烦,所以这里推荐另一种方式(有更好的方式欢迎留言),使用sys.path.append()的方法import sys,os sys.path.append(os.getcwd()) from src.src_test2 import Test2使用append的方式,将程序文件根目录放进了sys.path中,然后再引用绝对路径,这样的方式,不管使用上文中的第一或第二执行方式都可以调用,也可以单独编译test1文件,不用修改import路径,也是相对安全的方式。但是缺点就是,如果你修改了某一个包名,需要将所有引用地方都修改一下,工作量大,所以因地制宜。综上,详细讲解了相对路径和绝对路径的引用方法,现在你应该对import导入的问题有了清晰的理解吧备注:本文基于Python3.7版本测试
2021年02月05日
143 阅读
0 评论
0 点赞
2021-02-03
随机数大家都会用,但是你知道生成随机数的算法吗?
今天我们来和大家聊聊随机数。大家如果学过编程对于随机数应该都不陌生,应该或多或少都用到过。再不济我们每周的抽奖都是用随机数抽出来的,我们用随机数的时候,往往都会加一个前缀,说它是伪随机数,那么这个伪随机数的伪字该怎么解释,什么又是真随机数呢?真伪随机数目前学界划分真伪随机数的方式非常简单,一句话就能说明白,凡是用一定的算法使用程序生成的都是伪随机数,通过物理现象产生的随机数才是真随机数。也就是说计算学家们已经证明了仅仅依靠算法是无法生成真随机数的,也可以认为这是一个NP问题。算法生成的都是伪随机数的证明太过复杂我们可以不去深究,但是什么又叫做物理现象产生的随机数呢?其实也很简单,举个很简单的例子就是抛硬币和掷骰子。当然物理现象不止这些,比如还有电子元件的噪音、元素的衰变等等。真假随机数之间的最大差别在哪里?其实就在是否可以预测上。计算机算法得出的各种随机数之所以是伪随机数是因为它们的结果都是可以预测的,只要我们知道算法和起始状态以及各种参数,就可以预测下一次随机出来的结果。而真随机数则无法预测,就是纯粹随机的。但问题来了,抛硬币和掷骰子这些物理现象又是真的随机吗?如果我们知道了硬币的起始状态以及抛掷的角度和力度,是不是可以预测硬币抛掷的结果呢?进一步我们是否可以假设,如果我们能知道所有例子的所有状态,是否所有所谓的随机数都是可以预测的呢?但根据量子力学的测不准原理,我们知道我们无法同时知道粒子的位置和动量,不仅说明了我们无法预测,也说明了我们无法假设预测。所以某种程度上来说物理现象是不是就是真随机,这就成了一个哲学问题。但至少在计算机领域当中,这个问题是明确的,算法得出的都是伪随机数,只有通过物理现象得出的才是真随机数。在计算机系统当中,伪随机数都是有周期的,只要我们持续的次数足够多,就可以看到这种周期。而真随机数则不存在这种周期,有一位前辈做过一个随机数可视化实验,也就是把随机数得到的结果做成图片。我们可以直观地对比一下,这是真随机数可视化之后的图片:看起来像不像是以前的电视收不到信号的时候显示的内容?我们再来看看通过算法生成的伪随机数可视化之后的结果:对比一下还是挺明显的,明显可以看出来伪随机数是有规律的,这个规律体现出来就是图像当中的纹理。如果大家想要获取真随机数,可以访问random.org这个网站,它是免费的,我们可以人为设置上下限来获取指定范围内的随机数。对比过真伪随机数之后,我们再来看看现在计算机系统当中常用的伪随机数生成算法的原理。平方取中法我们首先介绍的是平方取中法,这个方法非常简单粗暴,是用来产生四位随机数的。具体的逻辑是怎样的呢?首先我们需要一个随机种子,比如2333,我们把这个随机种子进行平方,得到5442889。这个数一共有7位,我们给它左边填充一个0变成05442889,最后取出它的中间四位是4428,这就是我们随机得到的结果。当我们下次再计算随机数的时候,随机数的种子就成了4428。这个算法的作者是大名鼎鼎的计算机之父冯诺依曼,自从他确定了计算机体系结构之后一直沿用至今。他当时推崇这一算法的原因很简单,计算方便,速度快,也容易排查错误。它认为如果真的设计一个复杂的算法来生成看起来比较好的随机数,可能隐藏的bug比解决的问题还要多。seed = 2333 def random(): global seed seed = seed ** 2 return int(str(seed)[1:5])我写了代码实际运行了一下,结果看起来其实没有那么不靠谱。LCG算法冯诺依曼的随机数算法虽然看起来简单,但是非常草率,在很多场合下是显然不能使用的。所以人们又想出了新的算法,这个算法也很简单,看起来英文缩写高大上,其实翻译过来是线性同余法。也就是利用 来生成随机数。最后返回的结果是上述式子计算之后的结果,abc三个数都是我们选定的参数。当下一次随机的时候,就将上次的结果作为新的种子进行计算。我们写出它的递推公式就是:这个算法一眼就看明白了,它的核心完全在于abc这三个参数的选择。如果选的不好就不能实现随机数的效果,这里我给大家分享一个业内常用的选择,a=25214903917,b=11,c= 。这些数不是拍脑袋随便选的,而是计算学家们算出来的。实际上Java JDK当中Random的类采用的就是这样的算法。seed = 2 def lcg(): global seed seed = (25214903917 * seed) & ((1 << 48) - 1) return seed这种算法实现方式也非常简单,并且得到的效果也不错。如果要增加随机性,我们还可以在输出结果上做一些优化,比如进行位移或者是调换二进制位的顺序等等。但是这种算法也有缺点,就是它的计算方式是固定的,只是随机种子未知。只要愿意,我们是可以通过得到的随机结果去反推这些参数的。这并不是一个复杂的算法,因此LCG算法得到的随机数不能应用在一些高安全级别的应用上,否则可能会有安全隐患。梅森旋转算法LCG算法实现的伪随机数效果还不错,但是周期不够长,很容易被黑客推算出随机种子。后来两个日本学者又研究提出了新的伪随机数算法,在这个算法当中用到了梅森素数,所以称为梅森旋转算法。简单介绍一下梅森素数,梅森素数的意思是形如 的素数。利用梅森素数的性质可以设计出周期长度为梅森素数长度的随机数周期。比如目前Python、C++11等语言当中用的随机数计算包都是用的这种算法。目前常用的版本周期是 ,这是一个巨大的天文数字。梅森旋转算法的实现原理非常复杂,网上的资料也不多,我看过一些都不是非常好懂。这里就不介绍了,大家感兴趣可以去了解看看。但我个人觉得意义不大,因为实在是用不到,面试也完全不会考。虽然梅森旋转算法的周期非常非常长,但是仍不是安全的随机数算法,仍然有可能会被黑客破解。只不过和LCG算法相比,被破解的概率以及难度增加了许多。大家可能很好奇,什么样的算法才是安全的呢?其实业内的安全算法其实挺取巧的,一般的常用方法就是利用一个数学界的难题来设计一个算法。比如RSA加密算法,利用的就是大整数因式分解的问题。这样的问题业内除了暴力计算没有好方法,而暴力计算的复杂度非常非常高,根本不可能在有限时间内有解,自然这个就是一个安全的算法了。如果某位黑客有能力设计出破解的算法来,他根本也不用破解啥,只要把解法发表成论文,自然可以名利双收。你看随机数这么一个常见的功能下面居然隐藏了这么深的科学原理,而且更加震惊的是以我们人类如此厉害的文明,居然连随机一个数都做不到。不知道大家看到这里又有何种感受呢?
2021年02月03日
71 阅读
0 评论
0 点赞
2021-02-02
Kafka高可用,高吞吐量低延迟的高并发的特性背后实现机制
1 概述Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式消息系统,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。2 消息系统介绍一个消息系统负责将数据从一个应用传递到另外一个应用,应用只需关注于数据,无需关注数据在两个或多个应用间是如何传递的。分布式消息传递基于可靠的消息队列,在客户端应用和消息系统之间异步传递消息。有两种主要的消息传递模式:点对点传递模式、发布-订阅模式。大部分的消息系统选用发布-订阅模式。Kafka就是一种发布-订阅模式。2.1 点对点传递模式生产者发送一条消息到queue,只有一个消费者能收到 ,这种架构描述示意图如下:2.2 发布-订阅消息传递模式发布者发送到topic的消息,只有订阅了topic的订阅者才会收到消息,这种架构描述示意图如下:3 Kafka HA设计解析3.1 如何将所有Replica均匀分布到整个集群为了更好的做负载均衡,Kafka尽量将所有的Partition均匀分配到整个集群上。一个典型的部署方式是一个Topic的Partition数量大于Broker的数量。同时为了提高Kafka的容错能力,也需要将同一个Partition的Replica尽量分散到不同的机器。实际上,如果所有的Replica都在同一个Broker上,那一旦该Broker宕机,该Partition的所有Replica都无法工作,也就达不到HA的效果。同时,如果某个Broker宕机了,需要保证它上面的负载可以被均匀的分配到其它幸存的所有Broker上。Kafka分配Replica的算法如下:将所有Broker(假设共n个Broker)和待分配的Partition排序将第i个Partition分配到第(i mod n)个Broker上将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上3.2 消息传递同步策略Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader,然后无论该Topic的Replication 数量为多少,Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader pull数据。这种方式上,Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后,向Leader发送ACK。一旦Leader收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader向Producer发送ACK(前提消息生产者send消息是设置ack=all或-1)。为了提高性能,每个Follower在接收到数据后就立马向Leader发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被Consumer消费。Consumer读消息也是从Leader读取,只有被commit过的消息才会暴露给Consumer。Kafka Replication的数据流如下图所示:4 高可用机制4.1 kafka副本分区中的所有副本统称为AR(Assigned Repllicas)存储在zk中,所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集leader副本:响应客户端的读写请求follow副本:备份leader的数据,不进行读写操作ISR集合表:leader副本和所有能够与leader副本保持基本同步的follow副本,如 果follow副本和leader副本数据同步速度过慢(消息差值超过replica.lag.max.messages阈值 )或者卡住的节点( 心跳丢失超过replica.lag.time.max.ms阈值 ),该follow将会被T出ISR集合表ISR集合表中的副本必须满足的条件副本所在的节点与zk相连副本的最后一条消息和leader副本的最后一条消息的差值不能超过阈值replica.lag.time.max.ms如果该follower在此时间间隔之内没有追上leader,则该follower将会被T出ISR4.2 kafka通过两个手段容错数据备份:以partition为单位备份,副本数可设置。当副本数为N时,代表1个leader,N-1个followers,followers可以视为leader的consumer,拉取leader的消息,append到自己的系统中failover:当leader处于非同步中时,系统从followers中选举新leader( kakfa采用一种轻量级的方式:从broker集群中选出一个作为controller,这个controller监控挂掉的broker,为该broker上面的leader分区重新选主 )当某个follower状态变为非同步中时,leader会将此follower剔除ISR,当此follower恢复并完成数据同步之后再次进入 ISR5 kafka高吞吐机制5.1、零拷贝——Page Cache 结合 sendfile 方法,Kafka消费端的性能也大幅提升当Kafka客户端从服务器读取数据时,如果不使用零拷贝技术,那么大致需要经历这样的一个过程:操作系统将数据从磁盘上读入到内核空间的读缓冲区中。应用程序(也就是Kafka)从内核空间的读缓冲区将数据拷贝到用户空间的缓冲区中。应用程序将数据从用户空间的缓冲区再写回到内核空间的socket缓冲区中。操作系统将socket缓冲区中的数据拷贝到NIC缓冲区中,然后通过网络发送给客户端。非零拷贝从图中可以看到,数据在内核空间和用户空间之间穿梭了两次,那么能否避免这个多余的过程呢?当然可以,Kafka使用了零拷贝技术,也就是直接将数据从内核空间的读缓冲区直接拷贝到内核空间的socket缓冲区,然后再写入到NIC缓冲区,避免了在内核空间和用户空间之间穿梭。零拷贝可见,这里的零拷贝并非指一次拷贝都没有,而是避免了在内核空间和用户空间之间的拷贝。如果真是一次拷贝都没有,那么数据发给客户端就没了不是?不过,光是省下了这一步就可以带来性能上的极大提升。5.2、分区分段+索引Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的,通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行操作的能力5.3、批量发送kafka允许进行批量发送消息,producter发送消息的时候,可以将消息缓存在本地,等到了固定条件发送到kafka等消息条数到固定条数一段时间发送一次5.4、数据压缩Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩 压缩的好处就是减少传输的数据量,减轻对网络传输的压力6 kafka防止消息丢失和重复6.1 ack简介Kafka的ack机制,指的是producer的消息发送确认机制,这直接影响到Kafka集群的吞吐量和消息可靠性。ack有3个可选值,分别是1,0,-1,ack的默认值就是1ack=1,简单来说就是,producer只要收到一个leader成功写入的通知就认为推送消息成功了——至少一次(at least once): 消息不会丢失,但可能被处理多次。ack=0,简单来说就是,producer发送一次就不再发送了,不管是否发送成功——最多一次(at most once): 消息可能丢失也可能被处理,但最多只会被处理一次ack=-1,简单来说就是,producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了6.2 生产者丢失和重复6.2.1 数据丢失的情况当ack=0时,如果有一台broker挂掉,那么那台broker就会接收不到这条消息当ack=1时,如果有一台follower挂掉,那么这台follower也会丢失这条消息,或者follower还未同步leader的数据,leader挂了,也会丢失消息6.2.2 数据重复的情况当ack=-1时,只要有一台follower没有与leader同步,生产者就会重新发送消息,这就照成了消息的重复6.2.3 避免方法开启精确一次性,也就是幂等性, 再引入producer事务 ,即客户端传入一个全局唯一的Transaction ID,这样即使本次会话挂掉也能根据这个id找到原来的事务状态enable.idempotence=true 开启后,kafka首先会让producer自动将 acks=-1,再将producer端的retry次数设置为Long.MaxValue,再在集群上对每条消息进行标记去重!6.2.4 去重原理每个生产者线程生成的每条数据,都会添加由生产者id,分区号,随机的序列号组成的标识符: (producerid,partition,SequenceId),通过标识符对数据进行去重!但是只能当次会话有效,如果重启了就没有效果,所以需要开启事务的支持,由客户端传入一个全局唯一的Transaction ID,这样即使本次会话挂掉也能根据这个id找到原来的事务状态。6.3 消费者重复消费6.3.1 重复消费直接原因消费者是以维护offset的方式来消费数据的,所以如果在提交offset的过程中出问题,就会造成数据的问题, 即已经消费了数据,但是offset没提交6.3.2 解决办法手动维护offset加大这个参数kafka.consumer.session.timeout,以避免被错误关闭的情况加大消费能力在下游对数据进行去重7 kafka消息如何确定发送到Partition分区7.1 生产者在发送消息时指定一个partitionProducerRecord(String topic, Integer partition, K key, V value) Creates a record to be sent to a specified topic and partition ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) Creates a record to be sent to a specified topic and partition ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) Creates a record with a specified timestamp to be sent to a specified topic and partition ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) Creates a record with a specified timestamp to be sent to a specified topic and partition ProducerRecord(String topic, K key, V value) Create a record to be sent to Kafka ProducerRecord(String topic, V value) Create a record with no key7.2 kafka的消息内容包含了key-value键值对,key的作用之一就是确定消息的分区所在。默认情况下, kafka 采用的是 hash 取模的分区算法即 hash(key) % partitions.size7.3 既没指定partition也没有key 则”metadata.max.age.ms”的时间范围内轮询算法选择一个8 kafka消息如何确定Partition分区到Consumer如果所有的消费者实例在同一消费组中,消息记录会负载平衡到每一个消费者实例。如果所有的消费者实例在不同的消费组中,每条消息记录会广播到消费组中所有的消费者进程。如图,这个 Kafka 集群有两台 server 的,四个分区(p0-p3)和两个消费者组。消费组A有两个消费者,消费组B有四个消费者。c1、c2在一个group A中消费不同的P,同样group B也是这样,保证消费中某个节点丢失可以正常消费9 kafka强依赖于ZooKeeper的工作原理Kafka使用ZooKeeper的分布式协调服务,将生产者,消费者,消息储存(*broker**,用于存储信息,消息读写等)*结合在一起。同时借助zk,kafka能够将生产者,消费者和broker在内的所有组件在无状态的条件下建立起生产者和消费者的订阅关系,实现生产者的负载均衡。broker在ZooKeeper中注册kafka的每个broker(相当于一个节点,相当于一个机器)在启动时,都会在zk中注册,告诉zk其brokerid,在整个的集群中,broker.id/brokers/ids,当节点失效时,zk就会删除该节点,就很方便的监控整个集群broker的变化,及时调整负载均衡。topic在ZooKeeper中注册在kafka中可以定义很多个topic,每个topic又被分为很多个分区。一般情况下,每个分区独立在存在一个broker上,所有的这些topic和broker的对应关系都有zk进行维护consumer(消费者)在ZooKeeper中注册注册新的消费者,当有新的消费者注册到ZooKeeper中,ZooKeeper会创建专用的节点来保存相关信息,路径ls /consumers/{group_id}/ [ids,owners,offset],Ids:记录该消费分组有几个正在消费的消费者,Owmners:记录该消费分组消费的topic信息,Offset:记录topic每个分区中的每个offset监听消费者分组中消费者的变化 ,监听/consumers/{group_id}/ids的子节点的变化,一旦发现消费者新增或者减少及时调整消费者的负载均衡。10 kafka如何保证数据的顺序消费——案例10.1 关于顺序消费的几点说明:kafka的顺序消息仅仅是通过partitionKey,将某类消息写入同一个partition,一个partition只能对应一个消费线程,以保证数据有序。除了发送消息需要指定partitionKey外,producer和consumer实例化无区别。kafka broker宕机,kafka会有自选择,所以宕机不会减少partition数量,也就不会影响partitionKey的sharding。10.2 问题:在1个topic中,有3个partition,那么如何保证数据的消费?如顺序消费中的第①点说明,生产者在写的时候,可以指定一个 key, 比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。但是消费者里可能会有多个线程来并发来处理消息。因为如果消费者是单线程消费数据,那么这个吞吐量太低了。而多个线程并发的话,顺序可能就乱掉了。10.3 解决方案写N个queue,将具有相同key%的数据都存储在同一个queue,然后对于N个线程,每个线程分别消费一个queue即可。注:在单线程中,一个 topic,一个 partition,一个 consumer,内部单线程消费,这样的状态数据消费是有序的。但由于单线程吞吐量太低,在数据庞大的实际场景很少采用。
2021年02月02日
64 阅读
0 评论
0 点赞
2021-01-28
看了这篇,我确定你确实搞定了负载均衡
负载均衡的文章,网上文章其实已经很多了,每次都觉得某某文章讲的不错,可是一旦过段时间,啥都不记得了。那今天我们就用生活中的故事来聊聊负载均衡。文章中部分可能有点啰嗦,但是为了更好能让大家理解,我也是拼了,真真切切的想让大家掌握知识。什么是负载均衡?负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。负载均衡通常有两种目的:均摊压力和提供冗余(也可以理解为备份)。生活案列上面还看不懂的话,我们继续用生活案列来说:高速路出口处,如果只有一个出口时,突然有一天出现大量车辆(假设大家都没有办理ETC)这个高速出口下高速, 比如有几百两这会都要下高速,但是下高速要交过路费,每辆车至少也要耽搁几分钟,几百辆!!!意味着后面的可能要等几个小时,如果有多个出口呢?那就没必要等那么久了。如果在增加一个出口,这时候就是两个出口可以均摊车辆下高速,还得分收费员快慢,车辆3看到车1那边要快点,然后就跟上车1。如果再增加n个就可以想象效果了。但是太多了,貌似也会造成资源浪费,很多出口一天都没有几辆车出入,如果搞得太多岂不浪费,所以我们一般看到大多数都是两个,可以理解备用急用。「我们就把司机理解为负载均衡器,可以根据前方路况进行判别走哪个出口。判别的方法就可以理解为负载均衡算法。」用我们技术领域的术语叫做冗余。收费员的速度我就可以理解为我们系统某个服务的性能。技术领域下面用一张图来描述我们技术领域的负载均衡:结合生活中的场景和技术领域的场景一起理解更酸爽。注意:集群指的是我们同一个App应用服务的部署多个节点,集群的主要目的就是为了分担压力的。负载均衡器(系统)就可以理解为指挥员。来一个请求,指挥员把这个请求根据一定方法交给集群中的某个服务。指挥员就可以按照各种方式进行分配请求到集群中的某个服务。随机给、排队给、谁反应快给谁等方法,也就是形成了负载均衡算法。以上比喻仅仅是个人理解。负载均衡的种类DNS(Domain Name System 域名系统 )它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。DNS是最简单也是最常见的负载均衡方式,一般用来实现“地理级别”的负载均衡,比如说:北方人访问北京的机房,南方人访问广州的机房,西方人访问成都的机房。DNS负载均衡的本质是DNS解析同一个域名可以返回不同的IP地址。比如说:https://www.sina.com.cn/在北方的用户使用时会解析成10.210.1.12(北京机房)返回,南方的用户使用时会解析成14.213.164.27返回(广州机房)。DNS简单示意图优点配置简单,无成本费用将负载均衡的工作交给了DNS服务器,省去了管理的麻烦。缺点记录的添加与修改是需要一定时间才能够生效的(因为DNS缓存了A记录)。一旦有一台服务器坏了需要下线,即使修改了A记录,要使其生效也需要较长的时间,这段时间,DNS仍然会将域名解析到已下线的服务器上,最终导致用户访问失败。不能按需分配负载,DNS并不知道各服务器的真实负载情况,所以负载效果不是很好实际的情况:在实际的项目部署,我们一般会将部分服务器使用DNS解析,利用域名解析作为第一级负载均衡.再在服务器中使用nginx负载均衡作为第二级负载均衡。硬件负载均衡硬件负载均衡是通过单独的设备来实现负载均衡的功能,这类设备和路由器交换机有那么一些类似,更或者可以理解为一个用于负载均衡的基础网络设备。目前业界主要有两款硬件负载均衡:F5和A10。这类设备性能好,功能强大,但是价格可以用昂贵来形容,一般只有银行,国企等大型有钱的企业开会考虑使用此类设备,本人也只是在银行里见识过F5。至于A10没接触过就不撤了。优点功能强大:全面支持各层级的负载均衡,支持各种负载均衡算法,支持全局负载均衡。性能好:一般软件负载均衡能支撑10w+并发已经很不错了,但是硬件的负载均衡却可以支持100w+以上的并发。高稳定性:因为是商业品,所以经过了良好严格的测试,经过大规模的使用,所以稳定非常高。安全性高:硬件负载均衡设备除了能处理负载均衡以外,还具有防火墙、防DDOS攻击等效果。缺点价格昂贵:我记得之前银行购买F5花了上百万,据说还有更贵的,所以价格可想而知。扩展性不好:硬件设备可以根据业务进行配置,但无法进行扩展和定制化。软件负载均衡软件负载均衡是通过负载均衡软件来实现负载均衡功能的。常见的负载均衡软件有LVS和Nginx。其中LVS是Linux内核的四层负载均衡,四层和七层的区别在于他们协议和灵活性的不同。Nginx是7层负载均衡,支持HTTP,E-mail协议,而LVS是四层负载均衡,所以和协议无关,基本上所有应用都可以做到,比如说:聊天、数据库等。以下是Nginx的负载均衡简单示意图:优点nginx由C编写,同样的web服务器,占用的资源和内存低性能高。当启动nginx服务器,会生成一个master进程,master进程会fork出多个worker进程,由worker线程处理客户端的请求。nginx支持高并发,每个worker子进程是独立平等的,当有客户端请求时,worker进程公平竞争,抢到的worker进程会把请求提交给后端服务器,当后端服务器没有及时响应时,此worker进程会继续接收下一个request,当上一个请求有响应后会触发事件,此worker进程继续之前的执行,知道响应结束。一个request不会被两个worker进程执行。nginx支持反向代理(用户有感知的访问叫正向代理如使用vpn访问youtube,用户无感知访问叫反向代理如负载均衡),支持7层负载均衡(拓展负载均衡的好处)。nginx是异步非阻塞型处理请求(第三点印证),采用的epollandqueue模式,apache是阻塞型处理请求。nginx处理静态文件速度快(原因:nginx高度模块化,配置简单。nginx是单进程多线程)。缺点对比apache不稳定,由于是单进程多线程,进程死掉会影响很多用户。负载均衡有什么用?「流量分发」负载均衡能对多台主机流量进行分发,提高用户系统的业务处理能力,提升服务可用性「会话保持」在会话周期内,会话保持可使来自同一IP或网段的请求被分发到同一台后端服务器上。「健康检查」支持自定义健康检查方式和频率,可定时检查后端主机运行状态,提供故障转移,实现高可用;「负载均衡」解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);提高扩展性通过添加或减少服务器数量,提供网站伸缩性(扩展性);提高安全性安全防护,在负载均衡器上做一些过滤,黑白名单、防盗链等处理;常用负载均衡算法轮训负载均衡系统接收到请求后,按照一定顺序将请求分发给服务器上。轮训是一种简单的负载均衡算法策略,不会去关注服务器状态。优点:如果服务器都是正常的,那么轮训是最理想的,因为它会使得每个服务都得到相等量的请求,可以用"雨露均沾"来形容。缺点:上面的有点是理想状态的,但是现实往往不是那样的,现实还是很骨感滴,线上系统往往出现各种各样的问题,比如:当有一台服务器挂了,轮训算法不会管服务器状态,就是会导致大量的请求到一台已经挂掉的服务器上,从而导致系统不可用,进而造成用户流失。另外一种常见的问题就是有的服务器响应快,有的响应慢(比如32核的服务器和16核的服务器),轮训算法也不关注相应快慢,所以会导致很多服务请求响应时间慢,简单的导致用户体验不好,由于响应时间慢甚至可能拖垮其他系统。加权轮训负载均衡系统根据服务器权重进行请求任务分派到对应的服务器上,这里的权重一般是根据系统硬件配置进行静态配置的,采用动态的方式计算会更加适合业务,但是复杂度相比简单的轮训就高很多。加权轮训是轮训的一种特殊方式,主要目的是解决服务器处理能力的差异问题,比如:集群中有的服务器是32核,有的老系统却是16核,那么理论上我们可以对其进行权重配置值,即就是32核服务器的处理能力是16核的两倍,负载均衡算法权重比例调整为2:1,让更多的请求分发给32核的服务器。加权轮训解决了轮训算法中误服根据服务器的配置的差异任务进行更好的分配的问题,其实还是会存在无法根据服务器的状态差异性进行请求任务分配的问题。负载最低优先负载系统将请求分配给当前负载最低的服务器,这里的负载根据不同请求类型和业务处理场景,可以用不同的指标来衡量。比如以下几个场景,LVS这种4层网络负载均衡设备,可以以连接数来判断服务器的状态,服务器连接数量越大,表明服务器压力就越大。Nginx这种7层网络负载均衡系统,可以以HTTP请求数量判断服务器的状态(Nginx内置的负载均衡算法不支持这种方式,需要自行进行扩展)。如果我们是自己研发负载均衡系统,可以根据业务特点来选择衡量系统压力的指标。如果CPU是密集型,可以以CPU负载来衡量系统的压力;如果是IO密集型,则可以以IO负载来衡量系统压力。负载最低优先算法解决了轮训算法中无法感知服务器状态的问题,但是由此带来的代价是复杂度增加很多,比如:最少链接数优先的算法要求负载系统统计每个服务器当前简历的链接,其应用场景仅限于负载均衡接收的任何请求都会转发给服务器进行处理,否则如果负载均衡系统和服务之间是固定的连接池方式,就不适合采取这种算法。LVS可以采取这种算法进行负载均衡,而一个通过连接池的方式链接数据库Mysql集群的负载均衡系统就不适合采取这种算法进行负载均衡了。CPU负载均衡最低优先的算法要求负载均衡系统以某种方式收集每个服务器的CPU的具体负载情况,同时要确定是以一分钟的负载标准,还是以10分钟、15分钟的负载标准,不存在1分钟肯定比15分钟的好或差。不同业务最优的时间间隔也是不一样的,时间间隔太短容易造成频繁波动,时间太长可能造成峰值来临时响应缓慢。负载最低优先的算法基板上能够很完美解决了轮训算法的缺点,也因为采用负载最低优先算法后,负载均衡系统需要感知服务器当前运行状态,此时,同样造成代价上升很多。对于开发者来说也许轮训算法只要简短的代码就可以实现,然而负载最低优先算法需要大量的代码来实现。负载最低优先看起来是解决了轮训中的缺点,然后由于其复杂度的提升,导致真正使用中比例还不如轮训或者轮训加权算法。性能最优负载最低优先算法是站在服务器的角度来进行请求分配的,而性能最优算法是站在客户端的角度进行分配的,优先将请求分配给处理速度快的服务器,通过这种方式达到了最快响应给客户端。性能优先其实也负载最低优先有点类似,都是需要感知服务器的状态,与之不同的是性能最优是通过响应时间这个标准,在外部进行感应服务器状态而已,同样的实现复杂度也很高,主要体现在以下方面:负载均衡系统需要收集每次请求的响应时间,如果在大量请求处理的场景下,这种收集再加上响应时间的统计本身也会消耗系统的性能。为了减少这种统计上的消耗,可以采取采样的方式进行统计,即就是不用很完全的去统计所有服务器的所有请求时间,而是抽样统计部分任务的响应时间来估算整体请求所花的响应时间。采样统计虽然能减轻性能的消耗,但使得实现的复杂度增加了很多,因为要确定合适的采样率,采样率太低会导致数据的正确性,采样率高同样会造成性能的消耗,要找到一个合适的采样率的复杂度也是可想而知的。无论全部统计,还是采样统计,都需要选择合适的周期,是30秒性能最优还是1分钟最优?目前是没有标准的周期,都是需要具体业务场景进行决策,是不是感觉到了其复杂性,尤其是线上系统需要不断的调试,然后找出相对合适的标准。Hash类负载均衡系统根据请求中某些关键字进行hash运算,得到的相同值得分发到同一台服务器上去,这样做的目的主要是为了满足特定的业务需求,比如:源地址Hash:将来源于同一个IP地址的请求分配给同一个服务器进行处理,适合于存在事务、会话的业务。例如:当我们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是临时的,关闭浏览器后就会失效。网上银行后台无须持久会话信息,只需要在某台服务器临时保留这个会话就可以了,但需要保证用户在会话存在期间,每次请求都能访问在同一个服务器,这种业务场景就是通过源地址hash来实现的。ID hash :将某个ID表示的业务分配到同一台服务器上进行处理,比如:userId session id。上述的网上银行登录的例子,用session id hash可以实现同一个会话期间,用户每次都是访问同一台服务器上的目的。负载均衡算法应用Dubbo中使用了哪些负载均衡算法?Random LoadBalance(随机算法,默认)RoundRobin LoadBalance(权重轮训算法)LeastAction LoadBalance(最少活跃调用数算法)ConsistentHash LoadBalance(一致性Hash法)类图nginx中使用了哪些负载均衡算法?「round robin(默认)」:轮询方式,依次将请求分配到各个后台服务器中,默认的负载均衡方式。适用于后台机器性能一致的情况。挂掉的机器可以自动从服务列表中剔除。「weight」:根据权重来分发请求到不同的机器中,指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 例如:upstream bakend { server 192.168.0.14 weight=10; server 192.168.0.15 weight=10; } 「IP_hash」:根据请求者ip的hash值将请求发送到后台服务器中,可以保证来自同一ip的请求被打到固定的机器上,可以解决session问题。例如:upstream bakend { ip_hash; server 192.168.0.14:88; server 192.168.0.15:80; } 「url_hash(第三方)」:根据请求的url的hash值将请求分到不同的机器中,当后台服务器为缓存的时候效率高。例如:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法 。「fair(第三方)」:根据后台响应时间来分发请求,响应时间短的分发的请求多。例如:upstream backend { server server1; server server2; fair; } 总结我们用生活中的故事来讲述了负载均衡,讲述了什么是负载均衡,负载均衡的作用,负载均衡的种类,负载均衡算法种类,以及我们在Dubbo和nginx中负载均衡算法的应用。
2021年01月28日
76 阅读
0 评论
0 点赞
1
2
3
...
7