写点什么

架构师训练营 week5 总结 + 作业

用户头像
林毋梦
关注
发布于: 2020 年 07 月 09 日

总结

这周课程开始互联网应用的核心技术介绍,分布式缓存,消息队列和分布式数据库。从上周课程回顾的互联网应用架构发展历史中,我们知道这些技术的效能都已经过实践验证,是真实可行有效的好东西。当然,这并不是说可以无脑采用,还是要根据实际情况选择,找到合理的技术组合,满足各方面的要求是架构师的主要职责。

缓存

定义

缓存是位于数据访问者和数据源之间的高速存储,存储数据的复制集,可以在多次访问时提高访问速度。

缓存 vs 缓冲

缓存和缓冲都是高速存储,但是用途不同。缓存为了加速访问,其存储的是数据集的复制集。缓冲是为了匹配读写速度。当缓冲两端读写速度有较大差异时,缓冲的目的是临时保存一部分数据使得两边不阻塞。

分类

缓存是现代计算机体系架构的重要组成部分。在软件,硬件中都随处可见。

  • CPU缓存

  • L1缓存

  • L2缓存

  • L3缓存(主板)

  • IO缓存

  • 硬盘缓存

  • 操作系统缓存

  • 数据库缓存

  • JVM缓存

  • 应用程序缓存

  • 分布式对象缓存

  • 代理/反向代理缓存

  • CDN

原理

缓存就是一个KV存储,用键来查找返回值。HASH表提供O(1)的随机访问时间复杂度,而且一般缓存的内容是处理完毕可以直接使用的数据,所以从缓存中读取比直接从系统中获取数据更快。

关键指标

缓存效能的关键指标是命中率,也就是从缓存中取得数据的比率。命中率越高,缓存越有效。

影响命中率的因素主要有三个键集合大小,缓存空间大小和缓存对象生存时间。

键集合大小

缓存保存的是键值队,键是从缓存中获取值的唯一方法。键集合越大,被重复访问的概率就越低。冷热数据分离的本质其实就是对键集合按访问频率做划分,进而缩小键集合。一般来说,缓存很可能准从二八原则,时间局部性(最近被访问过的越可能再次被访问)和空间局部性(被访问过的键的附近很可能会被访问)。

缓存空间大小

缓存可用空间越大,能保存的键值对就越多,就越可能访问到键值,所以命中率越高。

缓存对象生存时间

生存时间(Time To Live, TTL)越长,键值在缓存中的停留时间越长,越可能被命中,所以命中率越高。

缓存使用模式

正确使用(本地)缓存的方式有4种。但是在分布式场景下缓存的使用就变得复杂了,最直观的方式是加分布式锁。

Cache Aside

就常见的缓存使用方式。就是在访问数据源前,先访问缓存,没有就从数据源加载并写入缓存,此后就可以直接通过访问缓存取值。在写入数据时,先写到数据源,然后更新(删除)缓存对象。

Read Through

数据源完全被缓存对应用屏蔽,应用直接读写缓存,缓存在内部控制数据加载和更新,Read Through是在读周期内更新缓存数据。

Write Through

类似Read Through,只是Write Through是在写周期内更新缓存数据。

Write back

Cache和数据源应被视为一个storage,由Cache负责回写数据到底层数据源。

分布式缓存

当单机缓存过载,无法再扩容后,分布式缓存作为水平扩展的方案出现了。最初的分布式缓存采用了多播同步的设计,但是这个方案带来网络负载,而且没有分区,最终使得缓存冗余,很低的内存利用率,完全没有解决容量过载的问题,所以很快就淘汰了。

之后出现了经典的基于一致性哈希的分区方案。一致性哈希是把键空间视作一个环,把存储节点地址的哈希值放置在环上对环分区,之后所有键都在环上沿顺时针方向寻址存储节点。原始的一致性哈希虽然做到了键集合的分区和较好的稳定寻址,就是说,当节点数量变化时,只有临近的节点受影响。但是,其缺点是划分不均匀,这使得节点变化时,特别是新增节点时,可能远不如预期。

对原始方案的改进是引入虚拟节点,也就是首先把虚拟节点平均的分配到物理节点上,然后用随机数把虚拟节点分配到键集合环上。基于随机数的均匀性,当数量足够大时,根据概率上的大数定理,基本上对环也做了均匀划分,这样每个物理节点上的键集合区间大小都会差不多大,比较均匀。

此处的关键时用随机数做虚拟节点的地址哈希值,看过不少同学的实现,其实都没有正确理解使用虚拟节点划分的本质是随机数的平均性带来的平均划分,而是错误用虚拟节点地址的哈希值作为环的划分,这样其实很难做到均匀,因为节点地址的哈希值在键空间上没有均匀性的理论基础。

一致性哈希并不是唯一的分区方案,但是它体现了分布式的一个关键点——分区(Partition),这个分布式的天然性质,在CAP理论下,无论是选择AP还是CP,分区都是不能舍弃的关键。

常见问题

缓存在读多写少的场景下,经常能显著的提高系统性能,但是有三个常见问题。

缓存穿透

当访问缓存和数据源中都不存在的数据时,所有访问都会落到数据源上,这就是缓存穿透。常见解决办法是缓存空值,并设置短TTL。

缓存击穿

当一个热点数据失效后,大量访问落到数据源上,造成巨大压力,这就是缓存击穿。常见解决办法是热点数据不过期,并在访问数据前加分布式锁。

缓存雪崩

当一批缓存对象短时间内一起失效,造成数据源访问压力突然上升,这就是缓存雪崩。常见解决办法是随机TTL,热点数据分片,不超时。

消息队列与异步

与缓存一样,另一个作为银弹存在的高并发神技就是消息队列与异步。异步是提高吞吐量的关键,消息队列是实现异步的常见方案。

异步的语义在不同语境下有所不同。IO中的异步(java中的aio)是kernel处理IO的方式,一共5种:同步阻塞,同步非阻塞,多路复用,事件驱动和异步IO。这里的异步是指生产者/消费者模型中,生产者不必等待消费者的处理结果。消息队列在这个模型中传递消息,使得两者解耦。同时,应为消息队列自身的容量,也可以起到削峰填谷的作用。正是因为消息队列所起的削峰填谷作用,它可以在高并发场景下为系统带来可控的压力,同时可以加入更多的消费者来平摊压力。

队列模式

最常见的对列模式有两种:点对点和订阅发布。

点对点模式

点对点模式并不是特指定向传递消息,而是指消息只能被一个消费者消费。也是有多种可靠性级别,比如至少一次,至多一次,一次且仅一次。

订阅发布模式

这种模式下,生产者发布消息到主题,消费者按主题接收消息。一个消息可以被多个消费者处理。生产者与消费者完全解耦,互不感知。可以通过增加消费者在不影响现有功能的情况下做扩展,是实现事件驱动架构(Event Driven Architecture, EDA)的典型方案。

作业

实验代码是Go语言实现,过程是按物理节点对虚拟节点1:1到1:500的比例对键集合环做划分,然后用100万个随机生成的字符串做10次实验。实验中,最小比是1:150,均方差为2006.9817028452342,而最佳比是1:411,均方差是1868.106111666156。

一致性哈希的Go实现:

package hash
import (
"fmt"
"github.com/dgraph-io/ristretto/z"
"github.com/theckman/go-securerandom"
"math"
"sort"
)
type Hash struct {
vNodeFactor int32
vNode map[int32]string
index IntSlice
}
func NewHash(v int32) *Hash {
return &Hash{
vNodeFactor: v,
vNode: make(map[int32]string),
index: make([]int32, 0),
}
}
func (h *Hash) AddNodes(nodes []string) {
for j := int32(0); j < h.vNodeFactor; j++ {
for i := range nodes {
h.AddNode(&nodes[i])
}
}
sort.Sort(h.index)
}
func (h *Hash) AddNode(node *string) {
if len(h.vNode) == 0 {
h.vNode[math.MaxInt32] = *node
h.index = append(h.index, math.MaxInt32)
return
}
i, _ := securerandom.Int32()
h.vNode[i] = *node
h.index = append(h.index, i)
}
func (h *Hash) GetNode(v string) (string, error) {
if len(h.index) < 1 {
return "", fmt.Errorf("no nodes")
}
node := h.vNode[math.MaxInt32]
if len(h.index) > 1 {
//idx := h.hash(v)
idx := int32(z.MemHashString(v))
for i := len(h.index) - 1; i >= 0; i-- {
if h.index[i] <= idx {
node = h.vNode[h.index[i]]
break
}
}
}
return node, nil
}
type IntSlice []int32
func (p IntSlice) Len() int { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

测试代码:

package main
import (
"fmt"
"github.com/gonum/stat"
"github.com/linwumeng/hash/hash"
"github.com/theckman/go-securerandom"
"math"
"time"
)
func main() {
n := randStrSlice(1000000)
now := time.Now()
for m := 0; m < 10; m++ {
rlt := make(chan struct {
m float64
n int
d map[string]int
})
for j := 1; j <= 500; j += 1 {
go func(f int) {
stats := make(map[string]int)
h := hash.NewHash(int32(f))
h.AddNodes([]string{"192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5", "192.168.0.6", "192.168.0.7", "192.168.0.8", "192.168.0.9", "192.168.0.10"})
for i := 0; i < len(n); i++ {
v, _ := h.GetNode(n[i])
stats[v] = stats[v] + 1
}
sd := stddev(stats)
rlt <- struct {
m float64
n int
d map[string]int
}{
sd,
10 * f,
stats,
}
}(j)
}
m := struct {
m float64
n int
d map[string]int
}{
math.MaxFloat64,
0,
nil,
}
for i := 0; i < 500; i++ {
s := <-rlt
if s.m < m.m {
m = s
}
}
fmt.Printf("best: %v - %v\n", m.n, m.m)
fmt.Printf(" %v\n", m.d)
fmt.Printf(" elasped: %v\n", time.Now().Sub(now).Milliseconds())
}
}
func stddev(stats map[string]int) float64 {
s := make([]float64, 0, len(stats))
for _, v := range stats {
s = append(s, float64(v))
}
return stat.StdDev(s, nil)
}
func randStrSlice(l int) []string {
n := make([]string, l)
for i := 0; i < len(n); i++ {
n[i], _ = securerandom.URLBase64OfBytes(32)
}
return n
}

测试结果:

best: 3500 - 2468.139425199116
map[192.168.0.1:100115 192.168.0.10:97174 192.168.0.2:105990 192.168.0.3:98701 192.168.0.4:100142 192.168.0.5:100249 192.168.0.6:99377 192.168.0.7:99775 192.168.0.8:97292 192.168.0.9:101185]
elasped: 29273
best: 3900 - 2413.1998581873727
map[192.168.0.1:95519 192.168.0.10:99538 192.168.0.2:105022 192.168.0.3:100899 192.168.0.4:98017 192.168.0.5:100412 192.168.0.6:99817 192.168.0.7:100259 192.168.0.8:101195 192.168.0.9:99322]
elasped: 57269
best: 4140 - 2069.2073630042764
map[192.168.0.1:101720 192.168.0.10:99157 192.168.0.2:98424 192.168.0.3:97787 192.168.0.4:102992 192.168.0.5:101643 192.168.0.6:101764 192.168.0.7:97888 192.168.0.8:97400 192.168.0.9:101225]
elasped: 85564
best: 4140 - 2060.7607548885653
map[192.168.0.1:97441 192.168.0.10:103250 192.168.0.2:100358 192.168.0.3:100899 192.168.0.4:100676 192.168.0.5:98411 192.168.0.6:98967 192.168.0.7:103167 192.168.0.8:99148 192.168.0.9:97683]
elasped: 113477
best: 4400 - 2262.3505966631747
map[192.168.0.1:97557 192.168.0.10:103497 192.168.0.2:100629 192.168.0.3:102107 192.168.0.4:100949 192.168.0.5:96635 192.168.0.6:98340 192.168.0.7:97783 192.168.0.8:101203 192.168.0.9:101300]
elasped: 141731
best: 4090 - 1937.2133479706245
map[192.168.0.1:96515 192.168.0.10:99890 192.168.0.2:100489 192.168.0.3:98786 192.168.0.4:103045 192.168.0.5:99326 192.168.0.6:101653 192.168.0.7:97896 192.168.0.8:101464 192.168.0.9:100936]
elasped: 169651
best: 1500 - 2006.9817028452342
map[192.168.0.1:101354 192.168.0.10:97605 192.168.0.2:98790 192.168.0.3:102625 192.168.0.4:99618 192.168.0.5:100485 192.168.0.6:97354 192.168.0.7:103348 192.168.0.8:100132 192.168.0.9:98689]
elasped: 197759
best: 4600 - 2446.764802754854
map[192.168.0.1:99698 192.168.0.10:99765 192.168.0.2:104086 192.168.0.3:103284 192.168.0.4:96326 192.168.0.5:98111 192.168.0.6:101255 192.168.0.7:100443 192.168.0.8:97289 192.168.0.9:99743]
elasped: 226240
best: 4380 - 2003.6769533368729
map[192.168.0.1:104856 192.168.0.10:100565 192.168.0.2:99923 192.168.0.3:97959 192.168.0.4:99476 192.168.0.5:97646 192.168.0.6:98865 192.168.0.7:100576 192.168.0.8:100622 192.168.0.9:99512]
elasped: 254273
best: 4110 - 1868.106111666156
map[192.168.0.1:99023 192.168.0.10:100634 192.168.0.2:99012 192.168.0.3:102056 192.168.0.4:96833 192.168.0.5:99894 192.168.0.6:100884 192.168.0.7:101286 192.168.0.8:97709 192.168.0.9:102669]
elasped: 282545



发布于: 2020 年 07 月 09 日阅读数: 49
用户头像

林毋梦

关注

还未添加个人签名 2018.08.25 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营week5 总结+作业