Kubernetes原理
容器的原理:Namespace做隔离,Cgroups做限制,rootfs做文件系统。
k8s的主要作用是调度容器,管理容器的生命周期,在k8s中调度的最小单位是pod,除此之外还有service、cluster这些概念。
容器的原理:Namespace做隔离,Cgroups做限制,rootfs做文件系统。
k8s的主要作用是调度容器,管理容器的生命周期,在k8s中调度的最小单位是pod,除此之外还有service、cluster这些概念。
平均负载是指单位时间内,处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。所以,它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程,因此和CPU使用率也并没有直接关系。
当平均负载为2时,意味着:
平均负载为多少比较合适?
CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。
比如:
CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。
Linux 通过 /proc 虚拟文件系统,向用户空间提供了系统内部状态的信息,而 /proc/stat 提供的就是系统的 CPU 和任务统计信息:
1 | $ cat /proc/stat | grep ^cpu |
而CPU使用率,就是除了空闲时间外的其他时间占总CPU时间的百分比,用公式来表示就是:
1 | CPU使用率 = 1 - 空闲时间 / 总CPU时间 |
但是这个值是开机以来的CPU使用率,没什么参考价值,重要的是计算单位时间内的CPU使用率或简称平均CPU使用率。
1 | 平均CPU使用率 = 1 - (空闲时间new - 空闲时间old) / (总CPU时间new - 总CPU时间old) |
平均CPU使用率可以通过top、ps命令来查看。
造成CPU使用率过高的原因可能是:
CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
根据任务的不同,CPU 的上下文切换可以分为几个不同的场景,也就是进程上下文切换、线程上下文切换以及中断上下文切换。
根据Linux的特权等级分级:
系统调用过程的CPU上下文切换:
需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。
进程上下文切换:
进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
进程上下文潜在的性能问题:
切换进程上下文的时机:
只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。
线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
当进程只有一个线程时,可以认为进程就等于线程。
当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
因此,根据切换的多个线程所属的进程不同,有2种情况:
同进程内的线程切换消耗的资源更少。
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
1、平均负载
系统平均活跃进程数,反映了系统的整体负载情况
2、CPU使用率
根据CPU上运行任务的不同,CPU使用率可以分为:
3、进程上下文切换
过多的上下文切换会将原本运行进程的CPU时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和回复上,缩短进程真正运行的时间,成为性能瓶颈。包括:
4、CPU缓存的命中率
包括L1、L2、L3等三级缓存。
uptime - 查看平均负载
平均负载最理想情况下等于CPU个数,超过时表示发生了过载,达到70%时就应该分析排查负载高的问题,。
watch - 可用于观察负载变化情况
top 或 读取/proc/cpuinfo - 查看系统有几个CPU
top - 查看CPU使用率
-w 查看进程上下文切换情况
sysstat包含mpstat、pidstat
mpstat 是一个常用的多核 CPU 性能分析工具,用来实时查看每个 CPU 的性能指标,以及所有 CPU 的平均指标。
pidstat 是一个常用的进程性能分析工具,用来实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。
主要用来分析系统的内存使用情况,也常用来分析CPU上下文切换和中断的次数
vmstat主要用于看系统整体的上下文切换情况,如果想看每个进程的详细情况,可以用pidstat
perf top 实时显示占用CPU时钟最多的函数或指令,因此可以用来查找热点函数
perf record、perf report 保存性能分析结果及展示结果
Future
刚开始接触并发编程,容易写出下面的代码:
1 | long startTime = System.currentTimeMillis(); |
此时开发很有可能会误以为结果是 1s 左右,毕竟 3 个FutureTask
同时执行、每个只等待 1s,结果确实应该是 1s,但是这里并没有实现并行化,因为future.get
是轮询调用的,第一个执行完毕后,第二个仍然需要等待 1s,因此结果是 3s。
虽然每个 FutureTask 是同时开始执行的,但是future.get
并不是同时开始等待的,如果想要达到并行执行的效果,一定是在上一个执行完毕的时候,下一个就已经执行完毕了,此时就可以直接获取结果了。
1 | long startTime = System.currentTimeMillis(); |
阻塞虽然看起来很廉价,似乎线程阻塞后就不占用资源了,实际上很多阻塞都是通过轮询标志位实现的,比如 FutureTask 内部实现了这样的一种状态机:
每次状态的变更都是通过CAS
(UNSAFE
)实现线程安全的,比如set
方法:
1 | protected void set(V v) { |
ListenableFuture 是在 JDK1.8 之前出现的,现在 CompletableFuture 一般是更好的选择。
1 | long start = System.currentTimeMillis(); |
LinkedBlockingQueue
;take
调用一直不返回,注意下面源码中的done
方法: 1 | private class QueueingFuture extends FutureTask<Void> { |
CompletableFuture
是对Future的加强,CompletableFuture
实现了 Future 接口,这意味着它本身也提供了通过阻塞或轮询获取结果的方式,相对 Future 来说,它的相对优势在于任务的编排,相对 CompletionService 来说,CompletableFuture
又有接口更灵活的优势。
1 | long start = System.currentTimeMillis(); |
注意上面的CompletableFuture.supplyAsync
和CompletableFuture.join
CompletableFuture.supplyAsync
,有2个方法,其中有个需要用户指定Executor,如果没有指定则使用JDK内置的线程池ForkJoinPool.commonPool()
AsyncSupply
封装AsyncSupply.exec()
CompletableFuture.internalComplete
CompletableFuture::join
CompletableFuture.waitingGet
q = new WaitNode(interruptible, 0L, 0L);
queued = UNSAFE.compareAndSwapObject(this, WAITERS, q.next = waiters, q);
ForkJoinPool.managedBlock(q);
阻塞,会调WaitNode.block()
来判断是否还阻塞着CompletableFuture.internalComplete
里的代码,会调LockSupport.unpark(t);
唤醒等待的线程桐生枝梨子(高显秘书、相貌平平、认真、火灾毁容)
里中二郎(死者、高显亲生骨肉)
克子
一原高显(社长、癌症)
一原苍介(高显弟弟、大学教授)
一原直之(苍介弟弟、精明)
一原纪代美(苍介另一哥哥的太太)
由香(纪代美女儿)
一原曜子(苍介妹妹)
加奈江(曜子女儿)
本间重太郎(高显朋友)
本间菊代(重太郎太太、现假扮)
一原健彦(苍介儿子、喜欢由香)
小林真穗(店长、高显情人)
矢崎警部
古木律师
鲹泽弘美(古木助理)
尹之壹
火灾
车祸逃逸
高显遗嘱
七七法事
八泽温泉(墓地)
枝梨子遗书
由香的死:拿走信封、之后死了、倒着的N(俄语字母)、刀伤、勒脖
红酒
安眠药
审讯
直之的珍珠领带夹
私生子
脚印
纪代美的一对珍珠
头发
茶道
碎冰锥
在搜索的时候,我们能通过搜索关键词快速得到结果集。当排序的时候,我们需要倒排索引里面某个字段值的集合,此时倒排索引无法发挥作用。换句话说,我们需要 转置 倒排索引。转置 结构在其他系统中经常被称作 列存储 。实质上,它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序。
ES有2种方法实现:
Doc Values | Field data | |
---|---|---|
何时创建 | 索引时,和倒排索引一起创建 | 搜索时动态创建 |
创建位置 | 磁盘文件 | JVM Heap |
优点 | 避免大量内存占用 | 索引速度快,不占用额外的磁盘空间 |
缺点 | 降低索引速度,占用额外磁盘空间 | 文档过多时,动态创建开销大,占用过多JVM Heap |
缺省值 | ES 2.x 之后 | ES 1.x 及之前 |
当 working set 远小于节点的可用内存,系统会自动将所有的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。
Doc Value默认是启用的,可以通过Mapping设置关闭
1 | PUT test_keyword/_mapping |
1 | "tweet": { |
1 | GET /_search?explain |
几种分页方式及应用场景
ES中的分页是从每个分片上获取from + size条数据,然后协调节点聚合所有结果,再选取前from + size条数据。
因为是from + size,所以from特别大时会有深分页问题。
解决办法是Search After:
1 | POST users/_search |
缺点是:
需要指定搜索sort:
Search After会通过唯一排序值定位,将每次要处理的文档数都控制在size个。
scroll
查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor
。
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图
一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc
来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。
启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。
1 | GET /old_index/_search?scroll=1m // 保持游标查询窗口一分钟。 |
这个查询的返回结果包括一个字段 _scroll_id
, 它是一个 base64 编码的长字符串。现在我们能传递字段 _scroll_id
到 _search/scroll
查询接口获取下一批结果:
1 | GET /_search/scroll |
这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为 1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards
。
当没有更多结果返回的时候,我们就处理完所有匹配的文档了。
缺点:
ES提供了几种默认的计算分值的函数:
设置权重
使用某个数值修改_score的值,比如乘以某个系数
原算分乘以某个字段得到最终结果,比如下面就是乘以原文档中的count字段
1 | GET doc/_search |
还可以根据某个函数来计算评分,比如如下命令新算分 = 老算分 * log(1 + factor * count)
:
1 | GET doc/_search |
为每一个用户使用一个不同的,随机算分结果
使用场景:让每个用户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序保持一致
1 | GET doc/_search |
以某个字段的值为标准,距离某个值越近,得分越高
自定义脚本完全控制所需逻辑
elasticsearch painless脚本评分
Elasticsearch中使用painless实现评分
1 | GET doc/_search |
计算目标文档和origin之间的距离
衰减函数
HBase是开源版的BigTable。
从官网下载HBase:https://hbase.apache.org/
注意兼容性:http://hbase.apache.org/book.html#hadoop
Hadoop安装后只包含HDFS和MapReduce,并不包含HBase,需要在Hadoop之上继续安装HBase。
编辑配置文件conf/hbase-site.xml
,可以修改数据写入目录:
1 | <configuration> |
将 DIRECTORY 替换成期望写文件的目录. 默认 hbase.rootdir 是指向 /tmp/hbase-${user.name} ,重启时数据会丢失。
编辑环境变量conf/hbase-env.sh
:
1 | # 如果JAVA_HOME已经有了就不用设置了 |
启动hbase:
1 | ./bin/start-hbase.sh |
关闭hbase:
1 | ./bin/stop-hbase.sh |
如果启动失败后者后续的命令执行失败了,可以查看根目录下的日志:
1 | vim logs/hbase-hgc-master-hgc-X555LD.log |
使用shell连接HBase:
1 | ./bin/hbase shell |
1 | # 查看命令列表,要注意的是表名,行和列需要加引号 |
create - 创建表、列族:
1 | # 创建表 |
list - 查询表信息
1 | > list |
put - 向表、行、列指定的单元格添加数据:
1 | # 向表t1中的行row1和列f1:c1所对应的单元格中添加数据value1,时间戳为1421822284898 |
get - 获取单元格数据
1 | # 从表t1获取数据,行row1、列f1,时间范围为TIMERANGE,版本号为1的数据 |
HBase的实现包含3个主要的功能组件:
客户端会访问HBase的服务端接口,并缓存已经访问过的Region位置信息,用来提高后续访问数据的速度。
Region服务器的主要职责:
Region一般采用HDFS作为底层文件存储系统,并依赖HDFS来实现数据复制和维护数据副本的功能。
每个Region都有一个RegionID来标识它的唯一性,要定位一个Region可以使用<表名, 开始主键, RegionID>的三元组。
HBase还会维护一张<Region标识符, Region服务器>的映射表,被称为元数据表,又名 .META.表。
如果一个HBase表中的Region特别多,一个服务器存不下.META.表,则.META.表也会被分区存储到不同的服务器上,并用一张根数据表来维护所有元数据的具体位置,又名 -ROOT-表,-ROOT-表是不能被分割的,永远只会被存储到一个唯一的Region中。
HBase中的行是根据行键的字典序进行维护的,表中包含的行的数量可能非常大,需要通过行键对表中的行进行分区(Region)。
Region包含了位于某个值区间内的所有数据,它是负载均衡和数据分发的基本单位,这些Region会被Master分发到不同的Region服务器上。
每个Region的默认大小是100MB200MB,当一个Region包含的数据达到一个阈值时,会被自动分裂成两个新的Region,通常一个Region服务器上会放置101000个Region。
Region服务器内部维护了一系列Region对象和一个HLog文件
每个Region由多个Store组成,每个Store对应了表中的一个列族的存储。
每个Store又包含一个MemStore和若干StoreFile,前者是内存缓存,后者是磁盘文件,使用B树结构组织,底层实现方式是HDFS的HFile(会对内容进行压缩)。
HLog是磁盘上的记录文件,记录着所有的更新操作。
每个Store对应了表中一个一个列族,包含了一个MemStore和若干个StoreFile;
其中,MemStore是在内存中的缓存,保存最近更新的数据;StoreFile由HDFS的HFile实现,底层是磁盘中的文件,这些文件都是B树结构,方便快速读取,而且HFile的数据块通常采用压缩方式存储,可以大大减少网络和磁盘IO。
HLog是WAL(Write Ahead Log),因此在MemStore之前写入
如果最后一次刷新后没有新数据,说明所有数据已经被永久保存。
如《缓存刷新》流程所述,每次MemStore刷新都会在磁盘上生成一个新的StoreFile,这样系统中每个Store都会有多个StoreFile,要找到Store中某个值就必须查找所有这些StoreFile文件,非常耗时。
因此,为了减少耗时,系统会调用Store.compact()把多个StoreFile合并成一个大文件。
这个合并操作比较耗费资源,因此只会在StoreFile文件的数量达到一个阈值时才会触发合并操作。
如《Region存储结构》所示,Region服务器是HBase的核心模块,而Store是Region服务器的核心,每个Store对应了表中的一个列族的存储,每个Store包含一个MemStore缓存和若干个StoreFile文件。
在分布式环境下,系统出错可能导致数据丢失,比如Region故障导致MemStore缓存中的数据被清空了。HBase采用HLog来保证系统故障时的恢复。