1 | git clone https://github.com/Yikun/spark.git |
1 | git remote add upstream https://github.com/apache/spark.git |
将apache/spark设置为远端分支,可以通过git branch -vva
查看分支情况。
1 | git checkout -b SPARK-123456 upstream/master |
在Apache社区,一般以JIRA的issue号作为分支名的标识,例如SPARK-123456
代表Spark项目的JIRA为123456的问题。
为了方便开发,我们可以基于上游分支upstream/master
创建SPARK-123456
本地分支。
可以通过git branch -vva
查看本地分支与远程分支的对应情况。
1 | git pull --rebase |
因为我们在第3、4步,已经建立了本地分支SPARK-123456
和上游分支upstream/master
的上下游关系,因此,这里我们只需要利用git pull --rebase
即可完成上游分支upstream/master
到本地分支SPARK-123456
的代码同步。
1 | git push origin SPARK-123456 |
将基于upstream/master
(apache/spark)上游远程分支的本地分支SPARK-123456
提交到自己仓库(Yikun/spark)的SPARK-123456
分支
使用vim ~/.gitconfig
修改[alias]
section.
git pr
1 | pr = "!f() { git fetch -fu ${2:-$(git remote |grep ^upstream || echo origin)} refs/pull/$1/head:pr/$1 && git checkout pr/$1; }; f" |
git pr 12
: 快速将Pull Request ID为12的代码下载到本地
git sync-upstream
1 | sync-upstream = !"git fetch upstream;git checkout master;git merge upstream/master;git push origin master" |
1 | git sync-upstream |
git sync-upstream
: 同步上游分支的master到自己仓库的master
托管类型 | 单文件限制 | 单仓库限制 | LFS单文件限制 | LFS单账户限制 |
---|---|---|---|---|
Github | 100MB | 建议小于1GB,强烈建议小于5GB | 2GB | 1GB |
Gitee | 50MB | 500MB | 仅对企业付费用户开放 | 仅对企业付费用户开放 |
简单说,如果gitee通过非LFS方式,上传了100MB以上的文件,那么github无法镜像。gitee通过LFS方式,上传了100MB以上的文件,最大不能超过2GB,且总和不能超过1GB。否则会出现this exceeds GitHub's file size limit of 100.00 MB
错误:
1 | root@yikun-x86:~/yikun/bigfile# git push origin main |
任何方案,都需要处理最近一次commit(通过删除或者LFS改造)和历史所有commits的提交(通过bfg)
找到大文件。通过git big-files
和git blob-find
找到所有出现问题的分支。
处理大文件:
[推荐] 方案一(删除大文件,并保留历史提交):
通过自动化下载和脚本的方式,此步完成后,所有的大文件在lastest commit都被清除。
方案二(利用LFS改造大文件,并保留历史提交):
lfs方式改造大文件。在每个分支通过git lfs
方式进行改造,此步完成后,所有的大文件在lastest commit都改造为lfs方式。
清除历史大文件。利用bfg工具,清除所有历史大文件提交记录。(注意:此步会重新提交每个commits,大文件的commit会被替换为xxx.remove的flag文件)
把下面的配置copy到~/.gitconfig的底部:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20[alias]
big-files = !"git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| sed -n 's/^blob //p' \
| sort -nk2 \
| cut -c 1-12,41- \
| $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest"
blob-find = "!f() { \
obj_name="$1"; \
shift; \
git log --pretty=format:'%T %h %s' \
| while read tree commit subject ; \
do \
if git ls-tree -r $tree | grep -q "$obj_name" ; \
then \
echo $commit "$subject"; \
git --no-pager branch -a --contains $commit ; \
fi; \
done; \
}; f"
git big-files
: 找出所有的大文件blob。1 | ➜ samples git:(dev) ✗ git big-files | tail -3 |
可以看到最大的文件的blob hash是96cf9e58c3e4
,大小是119MiB
,路径为c++/level2_simple_inference/2_object_detection/YOLOV3_coco_detection_VENC/model/yolov3.om
git blob-find
: 找到blob所在提交和分支。1 | ➜ samples git:(dev) ✗ git blob-find 96cf9e58c3e4 |
可以看到96cf9e58c3e4
这个blob,在dev分支的4290b1a、59d12cb这两个commits有提交或者修改。
方案一:删除大文件1
2
3
4rm -rf file
git add file
git commit
git push
方案二:通过lfs方式改造大文件1
2
3
4
5
6
7
8
9
10
11# 安装git lfs
git lfs install
# 跟踪大文件
git lfs track "*.psd"
git add .gitattributes
# 提交大文件
git add file.psd
git commit -m "Add design file"
git push origin main
参考:https://git-lfs.github.com/
https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/removing-sensitive-data-from-a-repository#using-the-bfg1
2
3
4
5
6
7
8
9# 注意:这里通过mirror方式clone,最后一步push会影响所有分支。
git clone --mirror git://example.com/some-big-repo.git
java -jar bfg.jar --strip-blobs-bigger-than 100M some-big-repo.git
$ cd some-big-repo.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
$ git push
为了使整个代码的阅读变得有趣,我们先提几个问题作为后续追寻蛛丝马迹的“导火索”:
从上图我们可以看到MapReduce的核心流程如上所示,从用户的Input文件到最终的Output文件,主要经历以下几个阶段:
Map阶段。
Split:会将用户的输入文件,进行一些“分割“,在client端进行,逻辑上进行分割,只记录偏移信息。
Map:Split文件信息会在Map阶段进行处理, 调用用户自己定义的Map函数。
环形缓冲区:Map的输出不会直接存在文件里,而是存在环形缓冲区中,攒够了以后再进行落盘。
Spill:从缓冲区落盘的过程叫做spill,也最终会生成多个Spill文件。
Map.out:Spill文件最终会被合并为最终的Map输出。
Shuffle阶段。
Shuffle阶段会将Map的输出下载到对应的Reduce的机器上。
Reduce阶段。
Merge:Reduce阶段最开始的时候,会将Map文件进行Merge,形成一个大文件,作为Reduce的输入。
Reduce:Reduce会执行用户自己定义的reduce函数,完成最终的输出。
更多详细的内容可以参考《Hadoop MapReduce Comprehensive Description》 [1] 这篇文章。
目前Hadoop支持的压缩算法共有2大类,一种是可分割的压缩算法,一种是不可分割的压缩算法。而支持的压缩算法的类型有:
[1] Hadoop MapReduce Comprehensive Description: https://0x0fff.com/hadoop-mapreduce-comprehensive-description/
在2020年9月26日,在Apache Hadoop Meetup上,我也分享了更多的技术细节,以及我们实际的性能测试结果:
安装依赖
1 | yum update -y |
新增Hadoop用户
1 | groupadd hadoop |
下载
1 | curl -L https://downloads.apache.org/hadoop/common/hadoop-3.3.0/hadoop-3.3.0-aarch64.tar.gz | tar zx |
环境变量准备
1 | export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.265.b01-6.oe1.aarch64 |
配置添加
cd $HADOOP_HOME/etc/hadoop/
core-site.xml
1 | <configuration> |
hdfs-site.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.name.dir</name>
<value>/home/hadoop/dfs/name</value>
</property>
<property>
<name>dfs.data.dir</name>
<value>/home/hadoop/dfs/data</value>
</property>
</configuration>
mapred-site.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<!--Deploy Hadoop on K8s, we need to specify a certain port range and expose by k8s Service-->
<property>
<name>yarn.app.mapreduce.am.job.client.port-range</name>
<value>60000-60010</value>
</property>
<property>
<name>yarn.app.mapreduce.am.webapp.port-range</name>
<value>60011-60020</value>
</property>
<property>
<name>mapreduce.application.classpath</name>
<value>$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*</value>
</property>
<!--
<property>
<name>mapreduce.map.output.compress</name>
<value>true</value>
</property>
<property>
<name>mapreduce.map.output.compress.codec</name>
<value>org.apache.hadoop.io.compress.ZStandardCodec</value>
</property>
-->
</configuration>
yarn-site.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>localhost</value>
</property>
<property>
<name>yarn.nodemanager.address</name>
<value>0.0.0.0:40555</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.nodemanager.env-whitelist</name>
<value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
</property>
<property>
<name>yarn.log-aggregation-enable</name>
<value>true</value>
</property>
</configuration>
1 | hdfs namenode -format |
启动所有服务1
start-all.sh
我们团队也在2020年年初时,对ZSTD压缩库进行了性能优化,最终优化已推入到Facebook的上游社区中,本文将详细的介绍我们进行的优化。
完整的Patch链接:facebook/zstd#2041
aarch64提供了一系列的neon指令,本次优化则利用了VLD和VST指令,借助neon寄存器进行读写加速,ARM的官方文档是这样描述这两个指令的:
VLDn and VSTn (single n-element structure to one lane)
- Vector Load single n-element structure to one lane. It loads one n-element structure from memory into one or more NEON registers. Elements of the register that are not loaded are unaltered.
- Vector Store single n-element structure to one lane. It stores one n-element structure into memory from one or more NEON registers.
来自ARM的官方文档Coding for Neon - Part 1: Load and Stores中,写的非常详细,引用一张图来描述neon寄存器和memory加载和存储的方式,核心思想就是:利用neon寄存器作为暂存的中转站,加速数据处理:
我们以u8的复制为例,总结下本次我们在ZSTD具体的优化实现:1
2
3
4
5
6
7static void ZSTD_copy8(void* dst, const void* src) {
vst1_u8((uint8_t*)dst, vld1_u8((const uint8_t*)src));
memcpy(dst, src, 8);
}
核心步骤包含两步:
这样便利用neon完成了对u8的memcpy的优化,对于此类优化,有兴趣的可以阅读What is the fastest way to copy memory on a Cortex-A8?,了解在Cortext-A8的架构下,如何快速的进行memory copy。
完成neon优化后,我们对压缩和解压缩都进行了测试,最终,在压缩场景获得了大概1+%的提升:
Average gains(level 1~19) | gcc9.2.0 | clang9.0.0 |
---|---|---|
Compression | 1.67% | 1.23% |
Decompression | 0.02% | 0.36% |
完整的Patch链接:facebook/zstd#2040
Prefetch的中文是预取,原理是通过将数据预取到cache中,加速数据的访问。一个比较常见的场景就是在循环中,我们可以通过显示的调用,充分的预取未来将会访问的数据或指令便能快速从Cache中加载到处理器内部进行运算或者执行。
在Jeff Dean的一次经典的talk–Software Engineering Advice from
Building Large-Scale Distributed Systems中,提到了cache和memory的速度差异,大致如下图所示:
可以看到,从cache中拿数据,将比直接从memory拿数据性能提升几十甚至上百倍,因此,我们也在本次的优化中,为aarch64加入的预取指令。
1 |
同时,将预取加速加入到了ZSTD_compressBlock_fast_generic和ZSTD_compressBlock_doubleFast_generic的主循环中,在数据访问前,预先先将数据加载到cache中,从而加速后续访问对数据读取。
我们仅对压缩进行了优化,因此,也仅对压缩进行了测试,测试结果可以看出,速度在aarch64架构下获得了1.5-3+%的提升:
Average gains(level 1~19) | gcc9.2.0 | clang9.0.0 |
---|---|---|
level 1~2 | 3.10% | 3.69% |
level 3~4 | 2.49% | 1.51% |
在Facebook的ZSTD中,我们使用了neon指令集对memcpy的过程进行了加速,同时,也利用了prefetch机制,加速了循环时数据的访问。
希望本篇文章,能够对大家带来一些性能优化的启发。
]]>最近,我们利用这一功能,将搭载着openEuler 20.03 (LTS) 操作系统,跑在Kunpeng 920 处理器的ARM环境接入进来,在近期华为与阿里合作的MPAM项目,也将充分的利用这些资源利用Github Action的能力完成构建与测试。
本篇文章将接入方法分享给大家,希望能够帮助更多同学们把自己的ARM环境也在Github上用起来。
资源的接入流程比较简单:
Settings
–Actions
进入资源接入页面,点击Add Runner
。根据弹出的提示,下载和运行脚本
完成后我们可以看到接入的资源:
我们为接入的项目增加一个Action:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21name: Run some script in Kunpeng env
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: self-hosted
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Run `uname -a` in Kunpeng env
run: |
uname -a
cat /etc/os-release
lscpu | grep -E "Architecture|Model name|CPU\(s\):"
这样,这个workflow是展示所接入的环境上内核、操作系统、处理器信息,我们可以从结果看到job的结果:
点击Details
可以进入详情页面:
可以看到,我们在资源上执行的指令,已经运行成功,可以看到这台资源的系统为openEuler 20.03 (LTS)
,CPU为aarch64 128核的Kunpeng 920
。
本文介绍了我们是如何将搭载着鲲鹏920处理器、openEuler操作系统的计算资源接入到Github Action的。可以看到Github Action的自定义资源接入,在ARM64下还是很顺滑的。
希望这篇文章能够帮助到大家,大家也可以尝试着将你们自己ARM资源接入进来,有问题可以留言一起讨论,玩的开心!:)
]]>最近在进行ARM切换的过程中发现了很多因为Java Math库在不同的平台上的精度不同导致用例失败,我们以Math.log为例,做一下简单的分析。下面是一个简单的计算log(3)的示例:
1 | public class Hello { |
我们发现,在x86下,Math的结果为1.0986122886681098
。1
2
3
4 on x86
java Hello
Math.log(3): 1.0986122886681098
StrictMath.log(3): 1.0986122886681096
而aarch64的结果为1.0986122886681096
。1
2
3
4 on aarch64
java Hello
Math.log(3): 1.0986122886681096
StrictMath.log(3): 1.0986122886681096
而在Java 8的官方文档中,对此有明确说明:
Unlike some of the numeric methods of class StrictMath, all implementations of the equivalent functions of class Math are not defined to return the bit-for-bit same results. This relaxation permits better-performing implementations where strict reproducibility is not required.
因此,结论是:Math的结果有可能是不精确的,如果结果对精度有苛求,那么请使用StrictMath。
在此,我们留下2个疑问:
the bit-for-bit same results
?better-performing implementations
的?为了能够更清晰的看到StrictMath的实现,我们深入的看了下JDK的实现。
我们从Math.log和StrictMath.log的实现为例,进行深入学习:
Math.log的代码表面上很简单,就是直接调用StrictMath.log。
1 | public static double log(double a) { |
StrictMath的代码,会调用StrictMath.c中的方法,最终会调用fdlibm的e_log.c的实现。
总体的实现和下图类似:
对于StrictMath来说,没有什么黑科技,最终的实现就是e_log.c的ieee754标准实现,是通过C语言实现的,所以在各个平台的表现是一样的,整个流程如图中蓝色部分。感兴趣的同学可以看e_log.c的源码实现即可。
回到我们最初的起点,再加上一个问题:
the bit-for-bit same results
?better-performing implementations
的?原来,JVM为了让各个arch的CPU能够充分的发挥自己CPU的优势,会根据架构不同,会通过Hotspot intrinsics替换掉Math函数的实现,我们可以从代码vmSymbols.hpp看到,Math的很多实现都被替换掉了。log的替换类似于:1
do_intrinsic(_dlog, java_lang_Math, log_name, double_double_signature, F_S)
最终,Math的调用为下图红色部分:
log的实现:
在x86下,最终其实调用的是assembler_x86.cpp中的flog
实现:
1 | void Assembler::flog() { |
而在aarch64下,我们可以从src/hotspot/cpu/目录下看到,aarch64并未实现优化版本。因此,实际aarch64调用的就是标准的StrictMath。
正因如此,x86汇编的计算结果的差异导致了x86和aarch64结果在Math.log差异。
当然,aarch64也在JDK 11中,对部分的Math接口做了加速实现,有兴趣可以看看JEP 315: Improve Aarch64 Intrinsics的实现。
在ARM优化过程中,有的是因为Math库和StrictMath不同的实现造成结果不同,所以我们如果对精度要求非常高,直接切到StrictMath即可。
但有的函数,由于在Java大版本升级的过程中,出现了一些实现的差异,先看一个简单的Java程序1
2
3
4
5
6public class Hello {
public static void main(String[] args) {
System.out.println("Math.toRadians(0.33): " + Math.toRadians(0.33));
System.out.println("StrictMath.toRadians(0.33): " + StrictMath.toRadians(0.33));
}
}
我们分别看看在Java11和Java8的结果:1
2
3$ /usr/lib/jvm/java-11-openjdk-amd64/bin/java Hello
Math.toRadians(0.33): 0.005759586531581287
StrictMath.toRadians(0.33): 0.005759586531581287
1 | $ /usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java Hello |
最后一位很奇怪的差了1,我们继续深入进去看到toRadians的实现:
Java8的实现为:
1 | // Java 8 |
1 | private static final double DEGREES_TO_RADIANS = 0.017453292519943295; |
原来在Java11的实现中,为了优化性能,将* 180.0 / PI
提前算好了,这样每次只用乘以乘数即可,从而化简了计算。这也最终导致了,Java8和Java11在精度上有一些差别。
我们可以通过repo的action的指引,快速的完成Github action的测试:
点击detail后,我们便可以完成Hello world的测试:
可以看到,整个过程非常简单,只需要提交一个位于.github/hello-wrold.yml的文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20name: CI
# 在push的时候触发这个workflow
on: [push]
# workflow的job内容
jobs:
build:
# 跑job的系统
runs-on: ubuntu-latest
# 定义需要跑的脚本
steps:
# 使用checkout action
- uses: actions/checkout@v2
# Hello World
- name: Run a one-line script
run: echo Hello, world!
# 打印多行
- name: Run a multi-line script
run: |
echo Add other actions to build,
echo test, and deploy your project.
我们可以点击detail进入Workflow的结果页面
官方的文档介绍的非常详细了,这里推荐一些文章,官方在action的文档主页中推荐了3个文章,这里简单介绍下:
About GitHub Actions
这篇文章主要从从使用者的角度对action进行了简单的介绍,通过这篇文章可以了解到使用Github action的一些注意事项(例如使用限制、通知机制等都在这里有提及)。
Configuring a workflow
这篇文章可以认为是对workflow的一个详解,并且对workflow中的关键概念进行了解释,把这篇文章读透,基本上Github Action的基础功能就了然于胸了。
About actions
这篇文章可以认为是写给action的开发者的一个入门文档,在你需要写一个action之前,建议通读这篇文章。
在这节会陆续更新下,我使用到的有用的功能:
在开源贡献的代码托管的过程中,我们有时候有需要将Github的代码同步到其他远端仓库的需求。具体的,对于我们目前参与的项目来说核心诉求是:以Github社区作为主仓,并且定期自动同步到Gitee作为镜像仓库。
结论1: 由于会被Github屏蔽,Gitee的自动同步功能暂时无法支持。
这个问题在Gitee的官方反馈中,建议github导入的项目能设置定时同步提及过,官方的明确答复是不支持。最近又再次和官方渠道求证,由于会被Github屏蔽的关系,这个功能不会被支持。本着有轮子用轮子,没轮子造轮子的原则,我们只能选择自己实现。
结论2: 靠手动同步存在时效问题,可能会造成部分commit的丢失。
Gitee本身是提供了手动同步功能的,也算比较好用,但是想想看,如果一个组织下面,发展到有几百上千个项目后,这种机制显然无法解决问题了。因此,我们需要某种计算资源去自动的完成同步。
结论3: 目前我们开源的好几个项目(例如Mindspore, OpenGauss, Kunpeng)都有类似的需求。
作为一个合格的程序员,为了守住DRY(don’t repeat yourself,不造重复的轮子)的原则,所以,我们需要实现一个工具,同步简单的配置就可以完成多个项目的同步。
最终结论:我们需要自己实现一个工具,通过某种计算资源自动的去完成周期同步功能。
其实调研结论有了后,我们面对的选型就那么几种:
PS:严格来讲,Github Action其实是第二种选择的子集,其实就是单纯的想体验一把,并且把我们的业务需求实现了。
Github Action提供了2种方式去实现Action:
Docker container. 这种方式相当于在Github提供的计算资源起个container,在container里面把功能实现。具体的原理大致如下:
JavaScript. 这种方式相当于在Github提供的计算资源上,直接用JS脚本去实现功能。
作为以后端开发为主的我们,没太多纠结就选择了第一种类型。关于怎么构建一个Github的Action可以参考Github的官方文档Building actions。官方文档真的写的非常详细了,并且也通了了hello-world级别的入门教程。
而最终的关键实现就是,我们需要定义这个容器运行的脚本,原理很简单:
大致就是以上4步:
关心细节的同学,具体可以参考代码:https://github.com/Yikun/gitee-mirror-action/blob/master/entrypoint.sh
举了个简单的例子,我们想将Github/kunpengcompute同步到Gitee/kunpengcompute上面,需要做的非常简单,只需要2步:
可以参考官方指引
1 | name: Gitee repos mirror periodic job |
可以参考鲲鹏库的实现。
上图,大概是每个阶段的原理和最终的效果。现在,这个使用Gitee Mirror Action的workflow已经运行起来了,可以在链接看到。
好啦,这篇硬核软文就写到这里,有同步需求的同学,放心使用。更多用法,可以参考Hub-mirror-action的主页Readme。
Github Action 官方链接:https://github.com/marketplace/actions/hub-mirror-action
代码仓库:https://github.com/Yikun/hub-mirror-action
有任何问题或者疑问,希望可以和大家一起改进这个Action,有问题可以直接提Issue或者PR,不用客气。
]]>我们先看看官方的解释:
The GNU C Library project provides the core libraries for the GNU system and GNU/Linux systems, as well as many other systems that use Linux as the kernel.
Glibc的全名是The GNU C Library,它为GNU系统、GNU/Linux系统以及提供了核心的底层库。比如,我们平常使用的memset
,strlen
等等这些非常常用的接口都由这个库提供。
在计算领域的水平场景,例如大数据、数据库、Web等领域都直接或者间接地依赖着Glibc,举个简单的例子,在数据库的代码中,我们经常使用memcpy接口,对变量进行复制,调用频次也异常的高。如果在数据复制的过程中,性能能够有所提升,那么对上层软件的性能提升也是显而易见的。
根据我们的分析,字符、内存和锁操作是最基础也是最重要的基本接口,因此,我们选择了对这三种类型的接口优先进行优化。在实现优化中,我们利用了Glibc的indirect function这一机制,即会根据CPU、CPU arch去自动选择匹配的函数。这一机制让我们的实现,更加灵活,也对现有系统影响最小。
下图为我们这次优化主要接口:
在上游社区的推进过程中,我们始终坚持Upstream First的原则,希望能够将鲲鹏优化的收益共享给整个生态,真正做到源于鲲鹏,回归社区。
所以,可以看到我们的优化大部分(橙色部分)都贡献到了上游社区的AArch64的generic实现中,从而使得整个生态都能够受益,而小部分(绿色部分)针对于Kunpeng CPU的特殊优化则保持了单独实现。
我们知道在一般的开发中,小字节数据操作的使用频率,是远远的大于大字节数据操作的使用频率,而对于大数据和数据库的场景,则有可能会出现很多大字节数据操作的使用。因此,其实我们的一个优化原则是:在保证中小字节没有负优化的前提下,提升大字节数据操作的性能。
本节我们将一一解析在我们贡献的过程中,每个接口优化的关键点,并且尽可能的写的通俗易懂,希望能通过这些干货,给大家在其他的优化中带来启发。
Patch链接:aarch64: Optimized implementation of memcmp
对于memcmp的优化,我们的核心思路是通过循环展开让每个周期内做的事情更多,从而减少循环本身的开销。下图可以直观的看出,循环展开带来的性能提升:
具体如下:
可以从我们实际的测试结果看到整体在中大字节的性能有不错的提升,尤其是在128字节以上的场景,性能提升更是达到了18%。
Patch链接:add default memcpy version for kunpeng920
memcpy优化,因为社区的falkor版本在大、小字节的性能表现,已经很完善,因此最终,我们直接使用了Flakor版本作为优化版本。
Falkor版本的将字符分为3种场景:
有兴趣的可以看看源码的实现链接,整体性能提升13-18%。
Patch链接:aarch64: Optimized implementation of memrchr
memrchr整体的优化思路是,参考memchr设计的魔鬼数字算法,通过汇编实现逻辑适配,实现对特定字符逆向查找的功能,替代原有的C语言实现方案达到优化,具体实现见上链接。
最终,我们获得了58%的性能提升,最终在大字节的场景,比generic版本提升了4倍左右。
Patch链接:aarch64: Optimized memset for Kunpeng processor.
我们进行了通过循环展开和特殊的定制优化来更好的适配硬件分支预测的特性,从而达到优化的效果。
特别说明的是,对于memset来说,置零场景是非常常用的场景,我们发现原有的实现使用DZ_ZVA指令并未在置零场景有显著效果,反而增加了许多条件分支,因此我们使用set_long代替了置零,由于set_long本身有更少的分支及更少的预测,所以性能与原实现比也有所提升。
Patch链接:aarch64: Optimized implementation of strcpy
strlen使用了neon寄存器,通过vector operations对函数进行了优化,对比原有的汇编实现,在64字节以上的场景,获得了5%-18%的提升:
strlen Patch链接:aarch64: Optimized strlen for strlen_asimd
strnlen Patch链接:aarch64: Optimized implementation of strnlen
strlen和strnlen同样使用了vector operations和循环展开,对主循环仅行了改造
strlen有7%-18%的提升:
strnlen有11%-24%的提升:
经过上面的介绍,相信大家已经了解了我们是怎么去优化这些函数的版本的,虽说大部分的优化都是比较晦涩的汇编语言,但是其实实际原理还是非常易懂的。
最后,我们再总结下我们应该从哪些方面考虑,去完成优化:
本书所提及的所有代码,均已贡献到Glibc上游社区,并且随着Glibc 2.31已经在社区完成发布,有需要的可以直接从社区上游获取使用,有任何问题也可以在本文留言。
另外,Glibc优化,也全部合入到集成在当前版本的openeuler中,有兴趣的,也可以直接使用openEuler最新版本进行体验。
]]>OpenLab提供的核心能力之一,就是可以为项目提供计算资源(目前以虚拟机为主),用户可以通过配置来完成任务的定义,那么,先以”Hello OpenLab”为例介绍如何使用OpenLab的资源。
首先,我们要通过配置文件,来告诉OpenLab,哪个项目需要跑什么脚本,具体如下:
https://github.com/Yikun/arm-openlab-test
的项目名称为Yikun/arm-openlab-test
1 | - project: |
脚本文件(.zuul/playbooks/arm-openlab-test/run.yaml):1
2
3
4
5
6
7
8- hosts: all
tasks:
- name: Hello OpenLab
shell:
cmd: |
set -xe
echo "Hello OpenLab"
executable: /bin/bash
为了让用户减少重复工作,OpenLab也提供了一些常用的脚本完成基本的安装。如下所示:
1 | - hosts: all |
用户只需要指定config-golang
,即可完成环境上golang的arm64版本的安装。1
2
3roles:
- role: config-golang
arch: arm64
当然,如果发现了更好的role,也可以直接贡献到OpenLab。
如上图所示,对于一共三个节点,然后还有一个128G的共享内存:
最简单的模型就是将每个Resource Rrovider(RP)看做独立的,当单个模型含有全部请求资源时,才算满足要求。即请求16个VCPU、16384MB的内存,那我们期待的就是获取到ID为1,6,7的节点。
对于嵌套模型,我们期望的是,将整个树的关系都能被发现到,当一颗树上的资源满足请求,即返回树。即请求16个VCPU、32768MB的内存、8个VF,那么我们最终得到的是1节点,他是作为满足条件树的根节点。
对于共享模型,我们期望的是,将共享的NFS的资源也考虑进去,当然,这个资源仅共享给一个agg分组的RP,即请求16个VCPU、32768MB的内存、128G的硬盘,我们期望得到的是1节点和6节点。
用户可以通过member_of指定agg,获取某个组内的资源。例如,我指定member_of agg1,那么我们期望得到的就是1节点和7节点。另外,还有一个场景,就是共享模型的提到的,即如果一个RP是共享的,那么在一个Aggregate中的RP都可以共享他的资源。
在Placment的实现中,也是将single模型和其他模型通过use_same_provider参数区分开来。我们从简单的入手,先看单一模型的实现。建议先找找刺激,看看代码,:)
核心流程主要有以下几个过滤条件:
当用户指定或禁止traits时,过滤包含指定traits且不包含禁止traits的resource provider
1 | if required_traits: |
我们看到SQL最开始的where条件就是traits和forbidden traits,放到最前面其实有个目的就是将大部分的RP都可以通过前面的条件过滤掉,这样提升了SQL的整体性能。
当用户请求某些资源时,保证RP的usage满足需求。可用量的检查类似如下过程:
a. VCPU < 16 剩余量检查,已使用+请求量<=(总量-预留量)*超分比
b. 1 < VCPU < 16 上下限检查,资源的分配粒度的检查,不能过大,不能过小。
c. VCPU % 1 分配步长,比如某些资源仅能5G,5G的分配。
1 | for rc_id, amount in resources.items(): |
当用户请求包含member_of分组信息时,仅获取aggregates的的resource provider。1
2
3
4
5
6
7
8
9
10# If 'member_of' has values, do a separate lookup to identify the
# resource providers that meet the member_of constraints.
if member_of:
rps_in_aggs = _provider_ids_matching_aggregates(ctx, member_of)
if not rps_in_aggs:
# Short-circuit. The user either asked for a non-existing
# aggregate or there were no resource providers that matched
# the requirements...
return []
where_conds.append(rpt.c.id.in_(rps_in_aggs))
上述过程完成后,一个Same Provider的过滤获取流程就走完了,最终包含的rp就是我们需要的信息,可以看出我们过滤了traits、forbidden traits、usage(inventory)、aggregates信息。
Note: 我们在阅读这种长SQL的代码时,一定要抓住where条件,从where条件入手,查看过滤的关键点,理解了整个SQL的大意之后,再根据where条件所需的信息,往前看一连串的join信息。
在上一节中,介绍了单一的Resource provider的获取流程,但是,当嵌套树、共享模型加进来后,事情变得复杂了一些。那么问题变成了,在考虑树状结构和分组信息的情况下,如何获取满足条件的树信息?
代码见nova/api/openstack/placement/objects/resource_provider.py#L3148-L3187
满足单个条件的Resource Provider(树的叶子节点)。
遍历每个资源类型,获取满足条件的provider id及对应的root id,例如,对于VCPU:16、VF:2的请求,我们会得到的是如下的rp列表:
a. 满足VCPU的(红色虚线框),即1, 6, 7。对应结果为:(1, 1), (6, 6), (7, 7)
b. 满足VF的(蓝色虚线框),即4, 5。对应结果为:(4, 1), (5, 1)
找出满足所有条件的树(树的根节点)。
在遍历所有用户请求的资源类型的过程中,生成了2个数据:
a. provs_with_inv,记录着所有满足资源的(provider_id, root_id, rc_id),这个是并集。对应结果为:[(1, 1), (6, 6), (7, 7), (4, 1), (5, 1)]
b. trees_with_inv,记录着满足所有资源请求的root_id,这个是取root的交集,即set([1, 6, 7]) & set([1, 1])。对应结果为:[1]
。
然后,根据tree_with_inv过滤provs_with_inv,一遍拿到最终满足条件的所有树(树的根节点)
最终得到的就是,即满足VCPU又满足VF的根节点。
如果在树的基础上再考虑共享的节点的话,事情复杂了那么一点点。
(共享独有步骤) 获取所有的共享RP。
首先最开始,捞了一把所有共享的RP。获取的方法比较简单,就是找到所有包含“MISC_SHARES_VIA_AGGREGATE”这一traits的RP,并且其剩余的可用量,满足我们的要求即可。
(在原有步骤追加) 在“满足单个条件的Resource Provider”追加共享信息。
在2.2.1的第1步完成时,继续append满足条件的共享信息。例如,对于VCPU:16、VF:2、DISK:128的请求,我们会得到的是如下的rp列表:
a. 满足VCPU的(红色虚线框),即1, 6, 7。对应结果为:(1, 1), (6, 6), (7, 7)
b. 满足VF的(蓝色虚线框),即4, 5。对应结果为:(4, 1), (5, 1)
c. (共享新增) 满足DISK的(紫色虚线框),即8。对应结果为:(8, 1), (8, 6)
(在原有步骤追加) 在“满足所有条件的树(树的根节点)”与共享信息再取交集。
追加上sharing的RP后,我们那两个关键的数据变为了:
a. provs_with_inv,记录着所有满足资源(包括共享资源)的(provider_id, root_id, rc_id),这个是并集。对应结果为:[(1, 1), (6, 6), (7, 7), (4, 1), (5, 1), (8, 1), (8, 6)]
,比之前新增了(8, 1), (8, 6)
。 即“anchors for sharing providers”这一方法完成的。
b. trees_with_inv,记录着满足所有资源请求(包括共享资源)的root_id,这个是取root的交集,即set([1, 6, 7]) & set([1, 1]) & set([1, 6]),比原来新增了set([1, 6]
。对应结果仍为:[1]
。
最终,就找到了满足条件VCPU:16、VF:2、DISK:128的树。
刚才我们提到了“anchors for sharing providers”,这个是干啥的?代码见nova/api/openstack/placement/objects/resource_provider.py#L433-L489
这个函数名字有点诡异,共享provider的“锚”?其实这个函数解决的就是:已知共享资源的情况下,如何找到可以使用这些共享资源的树根?
其实就是共享的RP与其关联的其他RP取笛卡尔积(即N*M)的过程,可能有点抽象,我们举个例子:
已知1、2、3处于一个AGG,然后,1、2共享了一些资源,我需要找到所有可以享用1、2资源的树。
1、2、3均处于同一agg 1中
1 | select resource_provider_id, aggregate_id from resource_provider_aggregates; |
通过agg自join取笛卡尔积
1 | select sps.resource_provider_id,sps.aggregate_id,rps.aggregate_id,rps.resource_provider_id |
在笛卡尔积的基础上关联root
然后,再将上述结果,和RP一join,就得到了关联的root。最终,发现可以共享1、2资源的跟节点就是1(1所在树根)、2(2所在树根)、0(3所在树根)
代码见nova/api/openstack/placement/objects/resource_provider.py#L3189-L3199
这个逻辑就很简单了,RP和agg一join,然后额外的条件就是RP在agg就行。
代码见nova/api/openstack/placement/objects/resource_provider.py#L3213-L3218
留下包含所有traits的RP,干掉包含任意forbidden traits的RP
又是一篇TL;DR的文章,就这样吧。。。
]]>1 | 1,2,3]) all([ |
1 | 1,2,3]) any([ |
1 | '中文') ascii( |
repr和eval为反向函数(类似序列化反序列化?)1
2
3
4
5
6
7
8
9
10
11
12'[1, 2, 3]') repr(
"'[1, 2, 3]'"
"'[1, 2, 3]'") eval(
'[1, 2, 3]'
'[1, 2, 3]') eval(
[1, 2, 3]
'[1, 2, 3]') repr(
"'[1, 2, 3]'"
1, 2, 3]) repr([
'[1, 2, 3]'
'[1, 2, 3]') eval(
[1, 2, 3]
大部分用法和bytes/str有点像,不过bytearray是可变的类似a list of char,str是immutable的。也就说说改变字符串的时候其实是把内存中真个对象换了,bytearray的话,只换某个字符,性能好一些
参考学习:Where are python bytearrays used?
A bytearray is very similar to a regular python string (str in python2.x, bytes in python3) but with an important difference, whereas strings are immutable, bytearrays are mutable, a bit like a list of single character strings.
This is useful because some applications use byte sequences in ways that perform poorly with immutable strings. When you are making lots of little changes in the middle of large chunks of memory, as in a database engine, or image library, strings perform quite poorly; since you have to make a copy of the whole (possibly large) string. bytearrays have the advantage of making it possible to make that kind of change without making a copy of the memory first.
But this particular case is actually more the exception, rather than the rule. Most uses involve comparing strings, or string formatting. For the latter, there’s usually a copy anyway, so a mutable type would offer no advantage, and for the former, since immutable strings cannot change, you can calculate a hash of the string and compare that as a shortcut to comparing each byte in order, which is almost always a big win; and so it’s the immutable type (str or bytes) that is the default; and bytearray is the exception when you need it’s special features.
bytes is an immutable version of bytearray
https://github.com/openstack/nova/blob/2d6a838/nova/virt/hyperv/driver.py#L75
1 | 'a') ord( |
classmethod must have a reference to a class object as the first parameter, whereas staticmethod can have no parameters at all.
1 | class Date(object): |
https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner
将一个字符串编译为字节代码,这个函数可以用在web应用从模板(可含Python语法)生成html。1
2
3
4
5>>>str = "for i in range(0,10): print(i)"
'','exec') # 编译为字节代码对象 c = compile(str,
c
<code object <module> at 0x10141e0b0, file "", line 1>
exec(c)
https://review.openstack.org/#/c/557369
在某个cell挂掉的时候,会影响跨Cell的查询、计算Quota等操作。在这个BP中提到了几个场景:
OpenStack oslo.db是OpenStack中处理DB相关功能的基础组件,基本OpenStack所有的核心组件都会使用这一基础库。
去年12月,遇到一个死锁的问题 #62 一个死锁问题的深入探究,研究了下oslo.db死锁重试的方式,发现其中并没有加入随机机制。在通信领域,有一个叫做“二进制退避机制”的算法(嗯,也算没白读了7年通信,哈哈,在本文立刻提升逼格),就是通过指数递增+随机的方式来解决无中心、多节点接入时产生的冲突的。
当时,顺着这个思路,我在oslo.db中提交了一个关于改进死锁重试机制的[patch/527362]:Improve exponential backoff for wrap_db_retry。4个多月后,终于合入了。写这篇文章主要是为了记录一下自己学习的过程,以及对死锁及其重试机制的思考。
我们先看看MySQL的文档中对死锁的定义:
A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.
大致意思就是说,死锁是由于每个事务持有另一个事务需要的锁,而导致不同事务都无法继续。因为两个事务都在等待资源变为可用,所以都不释放它拥有的锁。
一个非常形象的例子如下所示:
来自4个路口的车“死锁”了,每个路口的车都无法前进,因为自己前行的道路,都被别的路口的车堵住了,而自己因为无法前进也无法释放自己的道路。
当死锁发生的时候,我们能做什么呢?在Mysql的文档中How to Minimize and Handle Deadlocks,给出了一些建议,下面我列举几个通用和常见的条目:
- Always be prepared to re-issue a transaction if it fails due to deadlock. Deadlocks are not dangerous. Just try again.
如果发生了死锁错误了以后随时准备重试,死锁并不危险,放心大胆的重试吧- Keep transactions small and short in duration to make them less prone to collision.
让事务尽可能的小而短,减少冲突的可能。- Commit transactions immediately after making a set of related changes to make them less prone to collision.
提交事务的时候,也是能提交就尽可能地立刻提交- When modifying multiple tables within a transaction, or different sets of rows in the same table, do those operations in a consistent order each time. Then transactions form well-defined queues and do not deadlock. For example, organize database operations into functions within your application, or call stored routines, rather than coding multiple similar sequences of INSERT, UPDATE, and DELETE statements in different places.
当在一个事务中修改不同的表,或者表中不同行的时候,尽量保持一致的顺序。
另外,还提及了一些其他的应对策略,比如调整事务隔离的级别,锁的级别,优化索引的定义之类的,大多数是以预防为主。当发生死锁的时候,我们也应该首先想到是不是这些情况没有处理好,然而,当死锁真正发生的时候,我们还是用最土但最有效的方法去解决:重试!
在重试机制的实现中,重试时长的选择非常关键,有两个因素需要我们仔细思考一下:
(1) 退避机制-等待的基数时间。
目前时间的基数是,随着重试次数指数增长的,这个基数对于连接失败类的业务是比较有用的,想象一下,这种类型的业务我们重试的目的说白了就是:“过一段时间,试一试,看看能不能正常连上”。
退避策略的选择可以分为普通退避策略和指数退避策略。普通退避策略:也就是傻傻固定间隔的重试,比如,每次重试的时间都是x秒;
而还有一种方式就是指数退避:随着重试次数的增加,我们每次等待的时间也会逐渐递增,1秒,2秒,4秒,8秒,16秒等等。
在Exponential Backoff And Jitter一文中,也提到了指数退避、普通退避策略,并且附上了实际的仿真结果:
从图上看到,完成同样的事情,使用指数退避时,总的调用次数变少了。尤其在客户端竞争比较多的时候,指数退避的效果很明显。这个指数退避说白了就是:“再多等一会儿,别急这那么快就重试”。
(2) 随机因子-抖动时间窗口。
不加随机因子的问题是,即使我们大家一起等待了很久,但是还是同时去调用的,没有这个抖动,而单单的增加等待时间基数,只会激增等待时间,而对实际的冲突避免没有什么意思。
我们思考下,对于死锁这种场景来说,我们真的需要很长的等待时间吗?我觉得其实并不需要很长的“等待的基数时间”,我们需要的只是让各种死锁的请求,互相避开即可,所以其实,只需要拉长等待的时间窗口即可。
无随机的方式,就是oslo.db目前使用的方式,仅指数增长,等待时间为1秒,2秒,4秒,8秒。
在退避sleep的时候,加入随机机制,使得sleep的时间随机化,指数拉长调用的窗口,从而降低再次死锁概率。加上这个jitter后,等待时间变为0~1秒,0~2秒,0~4秒,0~8秒,0~16秒等范围内随机。
除了全量随机因子外,我们也可以选择顶部随机的方式,保底的等待基数时间随指数递增,在基数时间的上沿边界向下抖动(比如25%)。这种方法来说既保留了“安全”的重试时间,而且抖动时间窗口也在递增。例如我们25%的都抖动随机,等待时间就为1*0.75~1秒,2*0.75~2秒,4*0.75~4秒,8*0.75~8秒,16*0.75秒~16秒。
另外,在Exponential Backoff And Jitter一文中,介绍了各种随机时间窗口加到退避算法中的流程后,对平均时间和竞争的调用数也做了一个仿真。结果如下图所示:
很显然,其中,Fulljitter(0~2**n随机)的抖动范围大,平均抖动时间比较低,因此,从平均时间和冲突避免(总调用数)这两个指标综合看,Fulljitter是获胜的。但这并不意味着在所有情况下,我们都需要很低的时间间隔,更长的时间会拥有更“安全”的重试时间,代价则是更耗时了,我想这确实是一个值得思考的tradeoff。
随机重试时间的选取,我们需要更多的结合业务去看,如果重试的业务是由于竞争冲突引起的(就像死锁),那么,我们就要通过抖动的范围将冲突化解;而如果重试的业务是由于服务暂时不可用,但是可用的时间我们并不确定,这样我们就可以通过增加基数时间来避免无谓的尝试。
总结一下就是:通过指数退避机制递增的基数时间,来避免无谓的尝试,通过随机因子机制递增的抖动窗口,来减少冲突的可能。
在得到了充分的理论知识的洗礼后,我们回过头来看看,oslo.db的重试机制的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 重试的基数时间
next_interval = self.retry_interva
# 重试的最大次数
remaining = self.max_retries
while True:
try:
# 调用需要重试的函数
return f(*args, **kwargs)
except Exception as e:
# 是否继续重试,不继续就reraise
expected = self._is_exception_expected(e)
# reraise
# 休息next_interval秒
time.sleep(next_interval )
# 判断是否递增重试时间
if self.inc_retry_interval:
# 指数递增,并不超过最大重试时间
next_interval = min(next_interval * 2, self.max_retry_interval)
# 剩余次数
remaining -= 1
我将核心的代码提炼出来,我们来分析一下各个入参的作用
可以看到,目前的机制就是我们上文所提到的“指数退避机制”。也就是说,并没有增加随机因子jitter进来。
于是,这个优化的Patch就诞生了:Improve exponential backoff for wrap_db_retry。核心做了2件事情:
1 | if self.inc_retry_interval: |
其中,抖动时间的计算如下:1
2
3
4
5
6
7
8
9
10
11
12
13def _get_inc_interval(self, n, jitter):
# NOTE(jiangyikun): The "n" help us to record the 2 ** retry_times.
# The "sleep_time" means the real time to sleep:
# - Without jitter: sleep_time = 2 ** retry_times = n
# - With jitter: sleep_time = [0, 2 ** retry_times) < n
# 指数增加重试时间间隔
n = n * 2
# 全量随机抖动
if jitter:
sleep_time = random.uniform(0, n)
else:
sleep_time = n
return min(sleep_time, self.max_retry_interval), n
这个Patch也在4个月后的几天前,完成了合入。下面也记录一下相关的讨论:
在进行资源请求的时候,由于目前支持的能力有限,我们目前只能请求一个单一类型包含整数数量的资源。
例如,我们请求VCPU为2,内存为2G,要求其架构为X86架构,即通过以下URL进行请求:
GET /allocation_candidates?resources=VCPU:2,MEMORY_MB:2048&required=HW_CPU_X86_AVX
也不能指定我们需要某一个RP具有某种特质,所有不同类型的资源也只能从一个RP提供。
但是,在进行一些通用和嵌套的Resource Provider时,会有诸如下列的需求:
Requirement 1. 根据类型、特质来请求一个allocation,根据相同类型和不同特质来请求不同的多个allocation。
Requirement 2. 保证指定的资源来自同一个resource provider
Requirement 3. 在资源有限(高饱和度)的情况下,将allocations散布到多个resource provider(多个resource provider拼凑起来)的能力。
我们通过一个例子来说下这几个场景:
Use Case 1
我们希望请求一个在NET1上的VF,一个在NET2上的VF。
GET /allocation_candidates?resources=SRIOV_NET_VF:1&resources1=SRIOV_NET_VF:1
体现需求1要求的分组能力,在解析的时候,会将resource根据后面的number分组
Expect:
[RP1(SRIOV_NET_VF:1), RP2(SRIOV_NET_VF:1)]
[RP1(SRIOV_NET_VF:1), RP4(SRIOV_NET_VF:1)]
[RP3(SRIOV_NET_VF:1), RP2(SRIOV_NET_VF:1)]
[RP3(SRIOV_NET_VF:1), RP4(SRIOV_NET_VF:1)]
Use Case 2
请求一个带宽为10000 bytes/sec的vf
GET /allocation_candidates?resources1=SRIOV_NET_VF:1,NET_EGRESS_BYTES_SEC=1
体现需求二,来自同一个Resource Provider。每个分组,通过suffix来作为后缀,来区分resource,同一个组的资源后面number相同,期望请求同一个Resource Provider。
Expect:
[RP1(SRIOV_NET_VF:1), RP1(NET_EGRESS_BYTES_SEC:10000)]
[RP2(SRIOV_NET_VF:1), RP2(NET_EGRESS_BYTES_SEC:10000)]
[RP3(SRIOV_NET_VF:1), RP3(NET_EGRESS_BYTES_SEC:10000)]
[RP4(SRIOV_NET_VF:1), RP4(NET_EGRESS_BYTES_SEC:10000)]
Use Case 3
请求一个在NET1上带宽为10000bytes/sec的VF,并同时请求一个在NET2上贷款为20000bytes/sec的且网卡带有SSL加速的能力
GET /allocation_candidates?resources1=SRIOV_NET_VF:1,NET_EGRESS_BYTES_SEC=10000
&resource2=SRIOV_NET_VF:1,NET_EGRESS_BYTES_SEC=20000&required2=HW_NIC_ACCEL_SSL
体现了需求一和需求二,通过分组来获取不同的Resource Provider,通过同一分组编号来指定一个Resource Provider的能力。
Use Case 4
假设一个PF只剩2个VF了,请求一个NET1上4个VF。
GET /allocation_candidates?resources=SRIOV_NET_VF:4
体现需求三,内部会自动将4分割成2个2:
Expect: [RP1(SRIOV_NET_VF:2), RP3(SRIOV_NET_VF:2)]
这种表达方式我们称之为“Numbered Request Groups”,名字中包含了2个重要信息,一个是“Numbered”,对请求资源进行编号,另一个是“Groups”,根据编号对请求资源进行分组.
形如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21resources = { resource_classA: rcA_count,
resource_classB: rcB_count,
... },
required = [ TRAIT_C, TRAIT_D, ... ],
resources1 = { resource_class1A: rc1A_count,
resource_class1B: rc1B_count,
... },
required1 = [ TRAIT_1C, TRAIT_1D, ... ],
resources2 = { resource_class2A: rc2A_count,
resource_class2B: rc2B_count,
... },
required2 = [ TRAIT_2C, TRAIT_2D, ... ],
...,
resourcesX = { resource_classXA: rcXA_count,
resource_classXB: rcXB_count,
... },
requiredX = [ TRAIT_XC, TRAIT_XD, ... ],
目前的解析部分的核心实现在nova/api/openstack/placement/util.py#L349-L465。
目前已支持的参数有resources(对inventory请求)、required(对trait请求)、member_of(对aggregate请求)。
]]>PTG刚开完,没什么太多的事儿,jaypipes说了几点:
目前已经Freature Freeze了,因此,对于BP来说,没有什么太多更新了,只是简单的罗列了下相关的Patch。
Provider Tree series starting with: https://review.openstack.org/#/c/537648/
Nested RP traits selection: https://review.openstack.org/#/c/531899/
目前,Nested RP的这2部分工作也不会在Queens版本合入了,会推迟到Rocky。
Granular resource requests review: https://review.openstack.org/#/c/517757/
resource和requeired分组的支持,API部分的PatchQueens版本未完成。
Remove microversion fallback:https://review.openstack.org/#/c/528794/
由于目前Queens已经使用1.14作为默认的microversion,因此,对于之前的一些兼容版本不会再使用了,所以对之前的兼容代码进行了清理。
Use alternate hosts for resize:https://review.openstack.org/#/c/537614/
Alternate hosts已合入,上面是补了一些test case
Add generation support in aggregate association https://review.openstack.org/#/c/540447/
没有什么新的bug了,在之前讨论的aggregate相关的API增加generation的问题,cdent提了一个BP,会在Rocky版本完成。
placement server needs to retry allocations, server-side https://bugs.launchpad.net/nova/+bug/1719933
对于并发更新时的重试问题,还是有一些讨论,
@edleafe 认为,对于一些场景,请求aloocation时,用户认为有足够容量呀,不能够失败。
@jaypipes还是原来的意见:
it should “fail” in so much as a 409 Conflict is returned and allows the caller to retry if it wants.
也就是说,409肯定是要失败,重试的事情需要调用他的人来做。
当然,也会在PTG讨论下,generation到底怎么样去使用和暴露。已经把这个问题记到nova-ptg-rocky:Do we have a concurrency problem with PUT /allocations/{consumer_uuid} and/or POST /allocations ?
Placement queens summary https://anticdent.org/placement-queens-summary.html
Placement extraction https://anticdent.org/placement-extraction.html
@cdent 完成了两篇文章,一个是queens版本的placement总结,另外一个是cdent做的,关于将Placement从Nova抽离出来的一些工作。
关于将Placement抽离出来,大家发表了自己的看法:
@cdent 他认为,较早的把Placement分离出来,对于Placement和Nova来说都好,目前抽离的工作量比较小,好分离,另外,目前Nova投入的大量的时间和优先级放在Placement相关的事务上,分离出来,对Nova好一些。
@bauzas 不太同意现在去分离,他主要是担心Nova和Placement分离后,有点难协调。
目前的Feature的讨论,已经开始Rocky版本的了。
https://review.openstack.org/#/c/541507/
这个BP主要是希望为Glance增加Traits支持,在Glance的Properties中,增加类似”trait:HW_CPU_X86_AVX2=required”, “trait:CUSTOM_TRUSTED_HOST=required”的支持,让Placement调度的时候支持。
https://review.openstack.org/543062
efried写的一个bp,看名字知其意,调度的时候考虑Resouce Class的亲和。
Handle volume-backed instances in IsolatedHostsFilter:https://review.openstack.org/#/q/topic:bug/1746483+(status:open+OR+status:merged)
Matt发现了一个Filter的问题,主要是对volume-backed的情况进行一些异常处理。在Scheduler会议中,已经很久没有讨论过非Placement的问题。- -!
Add optional healthcheck middleware https://review.openstack.org/#/c/542992/
一个用于健康检查的midleware,对于API服务挺有用,尤其是对于LB场景下的检查活跃来说。
Feature的spec在这里:https://review.openstack.org/#/c/531456/
Glance image traits https://review.openstack.org/#/c/541507/
Resource class的亲和性 至少到S版本才会落(包括在Placement中支持NUMA亲和),优先级不高,提了下Placement RBAC的需求(Policy/RBAC support in Placement REST API)可能会更高一些。
update provider tree的优先级很高解决了很多问题
Placement returns 503 when Keystone is down https://bugs.launchpad.net/nova/+bug/1749797
Keystone挂的时候,Placement会返回一个503,这个问题最后是在keystone middleware里面加了一些detail信息: https://review.openstack.org/546108
调度失败的”Nova valid host”足够了吗?
@arvindn05 这哥们提到在虚拟机调度的时候,我们仅仅返回了”no valid host”,为啥不503一个,然后返回为啥调度失败。
@edleafe 说了2点,503肯定不合适,错误是用户,不是系统。详细信息不显示是因为不想把底层的硬件架构拓扑之类的信息暴露给用户。管理员可以通过日志之类的看到失败原因。
2018年的第一个team meeting,我们可以看到重点的工作还是在Nested Resource Provider这个BP,在这个时间,大家还是希望能够把Nested Resource Provider这个BP在Queens版本完成。
@jaypipes
解决了一个bug/1741125 Instance resize intermittently fails when rescheduling
https://review.openstack.org/#/c/531022/
dansmith增加了一个CONF.scheduler.max_placement_results,用于限制每次备选节点的请求,默认1000
https://review.openstack.org/#/c/531517/
在随后的开放讨论中,由于Resource Provider的aggregate信息在更新时,会有在不同节点上的多个请求并发进行更新的问题,我们需要一种方案去解决race conditions。是的,就是我们在 #65 提到的方法。
@2uasimojo(efried) 提到,这种方案并不是进程或者线程的锁,建议按照原来的实现,给更新RP的aggregate加上genration id,用于解决并发下的竞态更新问题。
即在PUT的时候,用户需要传入genration id,这个id就是Get时候的genration id。这种方案看似有点土,我更新个字段还得自己传genration,太不方便了。
但是,却是一种很好的方法来解决从Get直到PUT入库中间的竞争。
大家对这点,达成了一致,另外,我们在更新rp的aggregate的时候,仅更新正更新的rp的generation,而不需要更新aggregate中其他rp的genration。
最终,决定让 @cdent 去做generations-on-aggregate placement microversion相关的patch。
@2uasimojo(efried) 提出了这个问题,对于409的处理,一直不是很清晰,因为我们重试的时候,不知道到底应该是仅仅重试之前的操作,还是说再看看这个数据是不是已经更新之类的。
@jaypipes 说,发生409后,更新的调用者,需要回答一个问题“OK,我们需要更新的东西已经变了,在我进行重试时,检查一下我想要更新的东西是否已经更新过了”,所有的generation变化,只是表达了“something changed”,而不是“this thins changed”。所以在我们进行409的重试时,我们需要重读下所有的provider信息(比如traits、inventory等),然后检查下,我们想更新的东西是否已经存在了,如果是这样的话,我们什么都不做,如果没有,我们需要重新的调用update/set。
这个想要更新的状态取决于virt driver,和他希望做什么。(比如更新inventory和traits肯定是不一样的)。
总结来说,就是我们最初的设计:client-driven state retries,而不是傻傻的重试。
本次Meeting总的来说还是充满干货的,尤其是对generation和409重试的讨论。
NRP的进度没有太大进展,目前包含update_provider_tree和GET /allocation_candidates两部分内容。
这个是为了支持用户进行复杂资源请求的bp,最近会专门写一个文章记录一下其实现。
目前这个特性基本完成了,相关Patch:
patch/526436 Change compute RPC to use alternates for resize
bug/1743120: placement inadvertently imports many python modules it does not need
这个bug主要是说Placement导入了很多不需要的模块,主要是和Nova耦合太近,不利于后面拆分,并且直接使用Nova的也不够简洁。所以,清理、化简,保持干净。
Patch在这里:https://review.openstack.org/#/c/533244
主要为了对比ComputeDriver.update_provider_tree和缓存在report client的ProviderTree的变化。抽象出来了一个结构ProviderData,专门来返回数据。
总的来说,本次Meeting的讨论内容较少,集中在Nested Resource Provider上面。
重要事件:1月25日,Queens版本的Feature Freeze即将到来。
目前还是包括update_provider_tree series和Nested RP selection两部分。update_provider_tree series接近完成了(不包括resource tracker端到端的上报),Nested RP selection,会推到Rocky版本。
Nova中支持请求traits,另外这个请求也额外的提到了Granular resource requests特性,有部分功能是重合的,后续分析Granular resource requests时候,重点关注下。
Alternate hosts这个bp已经基本完成,后续也需要学习下。
https://review.openstack.org/#/c/528794/ 在Queens版本,nova默认支持1.14了,所以移除了一些之前版本的兼容代码。
Provider Tree series部分的工作已完成,https://review.openstack.org/#/c/533808/
First provider tree patch in progress: https://review.openstack.org/#/c/537648/ 这部分是端到端的从resource tracker中调用driver的update tree,应该会推到Rocky去做
Nested RP traits selection: https://review.openstack.org/#/c/531899/ 没有什么进展
从开放讨论中,@efried 提到,想要端到端的使用NRP,需要完成三部分:a. Resource Tracker刷新update_provider_tree b. jaypieps的NRP in alloc cands c. driver实现update_provider_tree。这三项工作,都没有在Queens完成,不过都比较接近完成了。
基本完成
完整实现推迟到Queens版本,https://review.openstack.org/#/c/517757/
https://review.openstack.org/#/c/537614/ 已经merge,至此,已经可以支持resize时候的alternate hostsl了
http://lists.openstack.org/pipermail/openstack-dev/2018-January/126653.html Matt提出希望用一种简单方法保持driver的兼容
2018年的第一个team meeting,我们可以看到重点的工作还是在Nested Resource Provider这个BP,在这个时间,大家还是希望能够把Nested Resource Provider这个BP在Queens版本完成。
@jaypipes
解决了一个bug/1741125 Instance resize intermittently fails when rescheduling
https://review.openstack.org/#/c/531022/
dansmith增加了一个CONF.scheduler.max_placement_results,用于限制每次备选节点的请求,默认1000
https://review.openstack.org/#/c/531517/
在随后的开放讨论中,由于Resource Provider的aggregate信息在更新时,会有在不同节点上的多个请求并发进行更新的问题,我们需要一种方案去解决race conditions。是的,就是我们在 #65 提到的方法。
@2uasimojo(efried) 提到,这种方案并不是进程或者线程的锁,建议按照原来的实现,给更新RP的aggregate加上genration id,用于解决并发下的竞态更新问题。
即在PUT的时候,用户需要传入genration id,这个id就是Get时候的genration id。这种方案看似有点土,我更新个字段还得自己传genration,太不方便了。
但是,却是一种很好的方法来解决从Get直到PUT入库中间的竞争。
大家对这点,达成了一致,另外,我们在更新rp的aggregate的时候,仅更新正更新的rp的generation,而不需要更新aggregate中其他rp的genration。
最终,决定让 @cdent 去做generations-on-aggregate placement microversion相关的patch。
@2uasimojo(efried) 提出了这个问题,对于409的处理,一直不是很清晰,因为我们重试的时候,不知道到底应该是仅仅重试之前的操作,还是说再看看这个数据是不是已经更新之类的。
@jaypipes 说,发生409后,更新的调用者,需要回答一个问题“OK,我们需要更新的东西已经变了,在我进行重试时,检查一下我想要更新的东西是否已经更新过了”,所有的generation变化,只是表达了“something changed”,而不是“this thins changed”。所以在我们进行409的重试时,我们需要重读下所有的provider信息(比如traits、inventory等),然后检查下,我们想更新的东西是否已经存在了,如果是这样的话,我们什么都不做,如果没有,我们需要重新的调用update/set。
这个想要更新的状态取决于virt driver,和他希望做什么。(比如更新inventory和traits肯定是不一样的)。
总结来说,就是我们最初的设计:client-driven state retries,而不是傻傻的重试。
本次Meeting总的来说还是充满干货的,尤其是对generation和409重试的讨论。
NRP的进度没有太大进展,目前包含update_provider_tree和GET /allocation_candidates两部分内容。
这个是为了支持用户进行复杂资源请求的bp,最近会专门写一个文章记录一下其实现。
目前这个特性基本完成了,相关Patch:
patch/526436 Change compute RPC to use alternates for resize
bug/1743120: placement inadvertently imports many python modules it does not need
这个bug主要是说Placement导入了很多不需要的模块,主要是和Nova耦合太近,不利于后面拆分,并且直接使用Nova的也不够简洁。所以,清理、化简,保持干净。
Patch在这里:https://review.openstack.org/#/c/533244
主要为了对比ComputeDriver.update_provider_tree和缓存在report client的ProviderTree的变化。抽象出来了一个结构ProviderData,专门来返回数据。
总的来说,本次Meeting的讨论内容较少,集中在Nested Resource Provider上面。
重要事件:1月25日,Queens版本的Feature Freeze即将到来。
目前还是包括update_provider_tree series和Nested RP selection两部分。update_provider_tree series接近完成了(不包括resource tracker端到端的上报),Nested RP selection,会推到Rocky版本。
Nova中支持请求traits,另外这个请求也额外的提到了Granular resource requests特性,有部分功能是重合的,后续分析Granular resource requests时候,重点关注下。
Alternate hosts这个bp已经基本完成,后续也需要学习下。
https://review.openstack.org/#/c/528794/ 在Queens版本,nova默认支持1.14了,所以移除了一些之前版本的兼容代码。
Provider Tree series部分的工作已完成,https://review.openstack.org/#/c/533808/
First provider tree patch in progress: https://review.openstack.org/#/c/537648/ 这部分是端到端的从resource tracker中调用driver的update tree,应该会推到Rocky去做
Nested RP traits selection: https://review.openstack.org/#/c/531899/ 没有什么进展
从开放讨论中,@efried 提到,想要端到端的使用NRP,需要完成三部分:a. Resource Tracker刷新update_provider_tree b. jaypieps的NRP in alloc cands c. driver实现update_provider_tree。这三项工作,都没有在Queens完成,不过都比较接近完成了。
基本完成
完整实现推迟到Queens版本,https://review.openstack.org/#/c/517757/
https://review.openstack.org/#/c/537614/ 已经merge,至此,已经可以支持resize时候的alternate hostsl了
http://lists.openstack.org/pipermail/openstack-dev/2018-January/126653.html Matt提出希望用一种简单方法保持driver的兼容
目前已经Freature Freeze了,因此,对于BP来说,没有什么太多更新了,只是简单的罗列了下相关的Patch。
Provider Tree series starting with: https://review.openstack.org/#/c/537648/
Nested RP traits selection: https://review.openstack.org/#/c/531899/
目前,Nested RP的这2部分工作也不会在Queens版本合入了,会推迟到Rocky。
Granular resource requests review: https://review.openstack.org/#/c/517757/
resource和requeired分组的支持,API部分的PatchQueens版本未完成。
Remove microversion fallback:https://review.openstack.org/#/c/528794/
由于目前Queens已经使用1.14作为默认的microversion,因此,对于之前的一些兼容版本不会再使用了,所以对之前的兼容代码进行了清理。
Use alternate hosts for resize:https://review.openstack.org/#/c/537614/
Alternate hosts已合入,上面是补了一些test case
Add generation support in aggregate association https://review.openstack.org/#/c/540447/
没有什么新的bug了,在之前讨论的aggregate相关的API增加generation的问题,cdent提了一个BP,会在Rocky版本完成。
placement server needs to retry allocations, server-side https://bugs.launchpad.net/nova/+bug/1719933
对于并发更新时的重试问题,还是有一些讨论,
@edleafe 认为,对于一些场景,请求aloocation时,用户认为有足够容量呀,不能够失败。
@jaypipes还是原来的意见:
it should “fail” in so much as a 409 Conflict is returned and allows the caller to retry if it wants.
也就是说,409肯定是要失败,重试的事情需要调用他的人来做。
当然,也会在PTG讨论下,generation到底怎么样去使用和暴露。已经把这个问题记到nova-ptg-rocky:Do we have a concurrency problem with PUT /allocations/{consumer_uuid} and/or POST /allocations ?
Placement queens summary https://anticdent.org/placement-queens-summary.html
Placement extraction https://anticdent.org/placement-extraction.html
@cdent 完成了两篇文章,一个是queens版本的placement总结,另外一个是cdent做的,关于将Placement从Nova抽离出来的一些工作。
关于将Placement抽离出来,大家发表了自己的看法:
@cdent 他认为,较早的把Placement分离出来,对于Placement和Nova来说都好,目前抽离的工作量比较小,好分离,另外,目前Nova投入的大量的时间和优先级放在Placement相关的事务上,分离出来,对Nova好一些。
@bauzas 不太同意现在去分离,他主要是担心Nova和Placement分离后,有点难协调。
目前的Feature的讨论,已经开始Rocky版本的了。
https://review.openstack.org/#/c/541507/
这个BP主要是希望为Glance增加Traits支持,在Glance的Properties中,增加类似”trait:HW_CPU_X86_AVX2=required”, “trait:CUSTOM_TRUSTED_HOST=required”的支持,让Placement调度的时候支持。
https://review.openstack.org/543062
efried写的一个bp,看名字知其意,调度的时候考虑Resouce Class的亲和。
Handle volume-backed instances in IsolatedHostsFilter:https://review.openstack.org/#/q/topic:bug/1746483+(status:open+OR+status:merged)
Matt发现了一个Filter的问题,主要是对volume-backed的情况进行一些异常处理。在Scheduler会议中,已经很久没有讨论过非Placement的问题。- -!
Add optional healthcheck middleware https://review.openstack.org/#/c/542992/
一个用于健康检查的midleware,对于API服务挺有用,尤其是对于LB场景下的检查活跃来说。
Feature的spec在这里:https://review.openstack.org/#/c/531456/
Glance image traits https://review.openstack.org/#/c/541507/
Resource class的亲和性 至少到S版本才会落(包括在Placement中支持NUMA亲和),优先级不高,提了下Placement RBAC的需求(Policy/RBAC support in Placement REST API)可能会更高一些。
update provider tree的优先级很高解决了很多问题
Placement returns 503 when Keystone is down https://bugs.launchpad.net/nova/+bug/1749797
Keystone挂的时候,Placement会返回一个503,这个问题最后是在keystone middleware里面加了一些detail信息: https://review.openstack.org/546108
调度失败的”Nova valid host”足够了吗?
@arvindn05 这哥们提到在虚拟机调度的时候,我们仅仅返回了”no valid host”,为啥不503一个,然后返回为啥调度失败。
@edleafe 说了2点,503肯定不合适,错误是用户,不是系统。详细信息不显示是因为不想把底层的硬件架构拓扑之类的信息暴露给用户。管理员可以通过日志之类的看到失败原因。
PTG刚开完,没什么太多的事儿,jaypipes说了几点:
最近,在处理Nova Metadata并发更新的问题(bug/1650188)的时候,发现Resource Provider的并发控制机制在最开始就考虑,是通过乐观锁的机制实现并发控制的,简单的说就是:
其实,这个方式就是我们常说的乐观并发控制(OCC, Optimistic Concurrency Control,也称作乐观锁)机制。
用户通过API对Resource Provider的资源进行更新时,会传入一个generation参数
curl -X PUT http://10.76.6.31/placement/resource_providers/7d2590ae-9999-4080-9306-058b4c915e32/traits -H “X-Auth-Token: $TOKEN” -H “OpenStack-API-Version: placement 1.16” -H “Accept: application/json” -H “Content-Type: application/json” -d ‘{
“resource_provider_generation”: 0,
“traits”: [“CUSTOM_YIKUN_TEST”]
}’
在最终的数据刷新时,完成事务提交前,会对generation进行刷新,例如对于本例中的traits更新,对应的代码在这里:nova/objects/resource_provider.py#def _set_traits,相当于做了一次检查,如果generation和用户预期的一致,更新成功,如果更新失败,则会raise并发更新失败的error。
如上图所示,如果操作A和操作B并发的请求进来,当A请求成功后,刷新了genration,这样,当B进行刷新的时候,就会刷新失败。
在Placement中,在对Resource Provider下的资源(例如allocation、inventory、trait等)进行修改时,均会对resource provider的generation进行刷新。我们看下实现的细节:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25def _increment_provider_generation(ctx, rp):
"""Increments the supplied provider's generation value, supplying the
currently-known generation. Returns whether the increment succeeded.
:param ctx: `nova.context.RequestContext` that contains an oslo_db Session
:param rp: `ResourceProvider` whose generation should be updated.
:returns: The new resource provider generation value if successful.
:raises nova.exception.ConcurrentUpdateDetected: if another thread updated
the same resource provider's view of its inventory or allocations
in between the time when this object was originally read
and the call to set the inventory.
"""
rp_gen = rp.generation
new_generation = rp_gen + 1
# 注意这里的更新条件,通过id及generation匹配
upd_stmt = _RP_TBL.update().where(sa.and_(
_RP_TBL.c.id == rp.id,
_RP_TBL.c.generation == rp_gen)).values(
generation=(new_generation))
res = ctx.session.execute(upd_stmt)
# 如果rowcount为0,说明已经不是之前的RP了
if res.rowcount != 1:
raise exception.ConcurrentUpdateDetected
return new_generation
并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
顾名思义,Nested Resource Providers,即嵌套的资源提供者。在Ocata版本,这个bp/nested-resource-providers就被提出,主要是为了使用户可以定义不同的Resource Provider之间的层级关系(hierarchical relationship)。
我们知道,目前Placement的功能已初具雏形,我们可以记录系统中可数的资源的总数。一个Resource Provider有一系列不同资源种类的存量信息(Inventory),也通过已分配量(Allocation)信息来记录已使用量。通过Resource Provider/Inventory/Allocation这三个关键模型,我们就可以解决以下几个需求:
如下图所示,一个计算节点包含8个CPU,500GB硬盘,16GB内存,已使用3个CPU,3GB硬盘,2GB内存,这个计算节点所属高IO组,具备SSD的能力,抽象为Placement模型后,若下图所示:
计算节点对应Resource Provider(蓝色),其包含的某种类型资源的总量对应Inventory(紫色),资源的类型对应Resource Class(灰色),已使用量对应Allocation(绿色),所属的组对应Placement组(黄色),计算节点的特质对应Trait(橙色)。
在之前的实现中,对于RP之间的关系,也仅仅支持aggregate功能。例如,某个RP可以把自己的资源,通过aggregate将RP的资源共享给同一aggregate的其他RP。这一功能对于共享存储、共享IP池之类的业务是满足需求的,但是,对于类似父子的这种关系,是无法支持的。
例如,在NUMA场景下,我们不但需要将主机的内存和VCPU资源记录,同时也需要记录每个主机上的某个NUMA的资源总量及消耗情况,这个就属于父子关系。
在nested-resource-providers中提到一场景:
there are resource classes that represent a consumable entity that is within another consumable entity. An example of such a resource class is the amount of memory “local” to a particular NUMA cell.
就是说一些类型代表一种资源消费的实体,同时,包含了另外一种资源消费的实体。举个例子就是memory这种resource class,这个memory是属于某个NUMA的。让我们通过一个例子来看下这个问题。
在一些对性能或时延有苛刻要求的场景,我们通常希望一个虚拟机能够部署到某个NUMA上(一个主机通常含有多个NUMA CELL,注意这个CELL和我们在Nova中说的Cell V2不是一个含义,而是NUMA独有的名词)。假设我们已经创建了一个叫做“NUMA_MEMORY_MB”的资源类型,资源的总量是192GB。当我们希望将它创建到一个磁盘大小充足并且NUMA内存充足的主机上时,如果仅考虑这个主机上的总内存,可能会找到一个并不是我们期望的主机。我们必须考虑每个NUMA CELL中的内存是否充足。
如上图所示,假设一个主机总共有192GB内存。其中128GB分配给了NUMA CELL0,另外64GB分配给了NUMA CELL1。
这时,现有的Placement机制,会汇总一个Resource Provider下面所有的某种资源的总和,即会查到的是主机上总内存,发现还有32GB,调度时,就认为这个主机可以作为备选主机。然而,实际我们从上图已经可以看出,实际每个NUMA CELL可供调度的内存均只有16GB,其实是不满足要求的。
在现有Resource Provider的基础上,实现这种嵌套关系,基础的数据模型非常重要。在关系数据库中,对分层数据进行管理,主要有2个模型:
有关邻接表和嵌套模型的内容可以参考Managing Hierarchical Data in MySQL和Join-fu: The Art of SQL。
Reousce Provider最终选择了邻接表作为基础数据结构,在etherpad中,轻描淡写的描述了下选择的原因:
A simple way of modeling this kind of nesting in a relational data store is to use something called an adjacency list model. We add a NULLABLE parent_resource_provider_id column to the resource_providers table to indicate that the resource provider is either a “top-level” provider (such as a compute host) or a “nested” provider (such as a NUMA cell on the compute host).
个人认为,选择这个模型的原因除了实现比较简单外,还有就是Resource Provider嵌套层级的不是非常深,即使进行一些查询时需要left join几次,也不会有非常大的性能损耗。当然,正如Jaypipes的PPT所述那样,比起Nest Sets来说,Adjacency list是very common but doesn’t scale。
另外,Managing Hierarchical Data in MySQL的“LIMITATIONS OF THE ADJACENCY LIST MODEL”一节中,提到了2个这个数据结构的限制,回头看来,在Placement进行设计时,都有应对的措施:
Working with the adjacency list model in pure SQL can be difficult at best. Before being able to see the full path of a category we have to know the level at which it resides. In addition, special care must be taken when deleting nodes because of the potential for orphaning an entire sub-tree in the process (delete the portable electronics category and all of its children are orphaned).
其一,是Full-path的遍历(例如,获取一个父树),我们必须要多次join,并且需要知道自己在第几层,从而决定join的次数。这个在Placement,加了一个root_id进行解决。
其二,是删除父类节点时,有可能造成底下的树被孤立了,这个Placement则是通过限制用户行为来解决的,即不允许删除有子节点的父节点。
因此,最终在Resource Provider的模型中,我们新增了2个field:
Indicates the UUID of the immediate parent provider. This will be None for the vast majority of providers, and for nested resource providers, this will most likely be the compute host’s UUID.
Indicates the UUID of the resource provider that is at the “root” of the tree of providers. This field allows us to implement efficient tree-access queries and avoid use of recursive queries to follow child->parent relations.
数据模型的Patch在这: patch/377138。
最简单的流程就是对Nest Resource Provider进行操作了,对于创建的流程来说,需要用户传递父亲节点,请求的格式类似:1
2
3
4{
"name": "Shared storage",
"parent_provider_uuid": "542df8ed-9be2-49b9-b4db-6d3183ff8ec8"
}
在创建的过程中,如果包含了父亲节点,那么,我们可以很方便的找到其对应的root节点,然后填到自己的root中(子节点和父节点有相同的root);如果没有父节点的话,那么这个子节点的根节点就是自己了(参考代码:nova/objects/resource_provider.py#L812-L819)。
同样的,在删除节点的时候,也需要考虑下嵌套的关系,在Resource Provider删除时,加了一个简单的限制:如果一个Resource Provider有子节点(参考代码:nova/objects/resource_provider.py#L824-L830),则不允许进行删除。
在 #63 中,我们提到过Nested Resource Provider的获取流程,参考Patch/534968大致的过程有以下几步:
由于目前Patch还在开发中,并且在估计最早要到Rocky版本才能完成,所以,等到全部完成后,再进行更详尽的介绍。
Placement的一个重要的接口,就是获取满足指定资源条件的allocation。举个例子,用户说,我需要1个VCPU,512MB内存,1GB磁盘的资源,Placement你帮我找找看看,有没有合适的资源
。
1 | curl -X GET http://10.76.6.31/placement/allocation_candidates?resources=DISK_GB:1,MEMORY_MB:512,VCPU:1 -H "X-Auth-Token: $TOKEN" -H "OpenStack-API-Version: placement 1.15" |
用户通过allocation_cadidates接口进行查询,参数是DISK_GB:1,MEMORY_MB:512,VCPU:1
,随后,Placement自己做了一大堆的查询之后,“告诉”用户:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31{
"allocation_requests": [{
"allocations": {
"f05575b2-3df6-4018-84f6-f2a75795b59b": {
"resources": {
"DISK_GB": 1,
"MEMORY_MB": 512,
"VCPU": 1
}
}
}
}],
"provider_summaries": {
"f05575b2-3df6-4018-84f6-f2a75795b59b": {
"resources": {
"DISK_GB": {
"capacity": 243,
"used": 6
},
"MEMORY_MB": {
"capacity": 11206,
"used": 3200
},
"VCPU": {
"capacity": 16,
"used": 8
}
}
}
}
}
上面就是我查找到的结果:
- 资源请求参数(allocation_requests):您请求资源,DISK有1GB,MEMORY有512MB,VCPU有1个。我帮你找到一个UUID为
f05575b2-3df6-4018-84f6-f2a75795b59b
的Resource Provider,满足这个条件。- Provider详细信息(provider_summaries):这个Resource Provider,它DISK总量为243GB使用了6GB,MEMORY总量为11206MB使用了3200MB,VCPU总个数为16个,已使用了8个。
整体流程如上图所示
下面以一个single reousrce provider(就是说不考虑嵌套、共享之类的)的请求来举个例子:
1 | SELECT rp.id |
此步完成后,就可以拿到满足给定条件的Resource Provider的ID了。
1 | -- 从inventory/allocation/resource_provider表取信息 |
查询的最终结果如下:
其实我们可以看到,这个接口的作用就是说,用户给一些资源的请求,然后placement就去查,满足条件的resource provider。
当然,作为placement最重要也是最复杂的接口之一,加上嵌套、共享、分组之后,情况变得越来越复杂,但是,核心的实现是差不多的,都是一个套路,join一系列我们需要的信息,然后在where判断这些信息是否满足条件,最后,把满足条件的RP返回给用户。
后续,我也会在分析学习独立的feature(比如Nested Resource Provider、Sharing Resource Provider等),加上此接口的变化点和深入解析。
]]>在Nova对虚拟机进行一些操作的时候,比如创建、停止虚拟机之类的操作的时候,会将这些事件记录在instance_actions表里面记录操作的时间、操作类型以及一些操作事件详情。
例如,我们可以通过instnace-action-list来查看虚拟机的操作,并可以通过对应的req id来查操作中的事件详情,如果是失败的话,还可以从事件详情中,看到对应的错误栈信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 nova instance-action-list e92885a9-06d6-4491-ac43-6fd04e32ee72
+--------+------------------------------------------+---------+----------------------------+
| Action | Request_ID | Message | Start_Time |
+--------+------------------------------------------+---------+----------------------------+
| create | req-416cb88e-5adb-4c0f-9c32-6370d3661940 | - | 2017-12-13T12:08:36.000000 |
| stop | req-52155da3-d2ca-463c-b380-6034c0b5fdf1 | - | 2017-12-13T12:09:17.000000 |
+--------+------------------------------------------+---------+----------------------------+
nova instance-action e92885a9-06d6-4491-ac43-6fd04e32ee72 req-52155da3-d2ca-463c-b380-6034c0b5fdf1
+---------------+--------------------------------------------------+
| Property | Value |
+---------------+--------------------------------------------------+
| action | stop |
| events | [{u'event': u'compute_stop_instance', |
| | u'finish_time': u'2017-12-13T12:09:23.000000', |
| | u'result': u'Success', |
| | u'start_time': u'2017-12-13T12:09:18.000000', |
| | u'traceback': None}] |
| instance_uuid | e92885a9-06d6-4491-ac43-6fd04e32ee72 |
| message | - |
| project_id | 0232cef222f7479fae3fd8fa24d8c382 |
| request_id | req-52155da3-d2ca-463c-b380-6034c0b5fdf1 |
| start_time | 2017-12-13T12:09:17.000000 |
| user_id | 5b0b6a4c068f4c1ba78b50d8a4db5057 |
+---------------+--------------------------------------------------+
在instance_action的表里面,记录着action的更新时间,比如event结束了,我们也期望能够action里面能记录update的时间,但是目前并没有进行刷新。
这个patch想做的事儿也比较简单,如上图所示,就是在event进行记录(比如开始和结束)的时候,也对action的更新时间也做刷新。也就是说,我们在写instance_event_action表后,也需要写instance_action表去记录下刷新时间。大致代码的关键逻辑如下所示(省略了一些无关的代码细节):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def action_event_finish(context, values):
"""Finish an event on an instance action."""
# 原有获取action
action = _action_get_by_request_id(context, values['instance_uuid'],
values['request_id'])
# 原有获取event
event_ref = model_query(context, models.InstanceActionEvent).\
filter_by(action_id=action['id']).\
filter_by(event=values['event']).\
first()
# 原有的event刷新流程
event_ref.update(values)
# **新增的刷新action时间逻辑**
action.update({'updated_at': values['finish_time']})
action.save(context.session)
return event_ref
在修复Nova的这个事件时间刷新问题(bug/507473)的时候,CI会概率性地挂一些用例,先开始以为是CI不稳定,workflow+1之后,最终的门禁检查一直过不了。Matt recheck了几次都是失败的,然后问:
I’m not sure if the test failures this patch is hitting are related to this change or not - they definitely don’t seem to be (I’m not sure why we’d get an UnexpectedTaskStateError during resize due to this change).
这才引起了我的注意,我找了下归档的日志发现:
Exception during message handling: DBDeadlock: (pymysql.err.InternalError) (1213, u’Deadlock found when trying to get lock; try restarting transaction’) [SQL: u’UPDATE instance_actions SET updated_at=%(updated_at)s WHERE instance_actions.id = %(instance_actions_id)s‘] [parameters: {‘instance_actions_id’: 23, ‘updated_at’: datetime.datetime(2017, 12, 4, 2, 48, 36, 91068)}]
第一反应是,我去!死锁了?简单的一个update怎么会死锁?确认了下where条件比较单一,并不是因为条件排序不稳定引起的死锁;也确认了下action数据库的索引,也比较简单,也不会有死锁问题。然后,就看业务代码,代码逻辑也很简单,一个事务里面包含了4件事,2个查询,2个刷新。不科学啊!
遇到这种活久见的问题,最好的办法就是把每一句SQL都dump出来,因为不是裸写SQL,鬼知道SQLAlchemy中间的ORM那层为我们做了什么。
OpenStack的oslo.db为我们提供了一个配置项:1
2
3[database]
# (Integer) Verbosity of SQL debugging information: 0=None, 100=Everything.
connection_debug = 100
把他设置成100就可以dump出执行的每一句SQL了,这个方法在我们进行调试的时候很方便。然后,进行复现,结果让我震惊了(问号脸???代码是一样的,生成SQL的顺序却是不一致的):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19-- 从API dump的结果
BEGIN (implicit)
SELECT ... FROM instance_actions WHERE ...
SELECT ... FROM instance_actions_events WHERE ...
-- 先刷新action
UPDATE instance_actions SET updated_at=%(updated_at)s WHERE instance_actions.id= %(instance_actions_id)s
-- 再刷新action_event
UPDATE instance_actions_events SET updated_at=%(updated_at)s, finish_time=%(finish_time)s, result=%(result)s WHERE instance_actions_events.id = %(instance_actions_events_id)s
COMMIT
-- 从Conductor dump的结果
BEGIN (implicit)
SELECT ... FROM instance_actions WHERE ...
SELECT ... FROM instance_actions_events WHERE ...
-- 先刷新action_event
UPDATE instance_actions_events SET updated_at=%(updated_at)s, finish_time=%(finish_time)s, result=%(result)s WHERE instance_actions_events.id = %(instance_actions_events_id)s
-- 再刷新action
UPDATE instance_actions SET updated_at=%(updated_at)s WHERE instance_actions.id= %(instance_actions_id)s
COMMIT
完整的SQL dump我贴在了paste/628609,可以分析出来,就是产生死锁的根本原因:在一个事务中,更新2个表的相反行。并发执行2个这样的事务,一个事务拿着action表的行锁,一个事务拿着action_event表的行锁,它们都互相等着对方释放,最终产生了死锁,如下图所示。
从MySQL官方DOC里,给的建议How to Minimize and Handle Deadlocks中,我们也看到了类似的建议:
When modifying multiple tables within a transaction, or different sets of rows in the same table, do those operations in a consistent order each time. Then transactions form well-defined queues and do not deadlock. For example, organize database operations into functions within your application, or call stored routines, rather than coding multiple similar sequences of INSERT, UPDATE, and DELETE statements in different places.
核心意思就是说,我们在一个transaction中更新多个表的时候,或者说在一个表中更新不同行的时候,一定要保证每一次调用的顺序是一致的。最终,临时解决这个问题的方式也比较简单,就是在这个函数上加一个死锁重试装饰器,即在发生死锁的时候进行重试,CI终于全绿了。
问题解决就结束了吗?不,2个疑问一直在心中徘徊:
- 死锁重试的装饰器是怎么实现的,真的有效吗?
- SQLalchemy做了什么导致最终生成SQL的顺序是不稳定的,为什么要这么做?
这个问题的场景和上研的时候,在通信中搞的“退避算法”很类似(HINOC中信道接纳的时候,多个节点并行接纳时,如果发生冲突,需要退避重试),都是冲突避免,通信中是避免信道冲突,而这里则是避免数据库的死锁。
我们从oslo_db/api.py可以看到他的实现,原理比较简单,就是隔几秒(2的retry数次方秒),如果调用成功,就终止重试。伪代码大概如下:1
2
3
4
5
6
7
8
9
10t = 1
for i in range(try):
sleep(t)
# 指数递增
t = t * 2
# 超过上限取上限
t = min(max_t, t)
func()
if not raise deadlock:
break
虽然看着隔了一些时间,但是,这种指数递增的机制对于死锁这种问题没有什么卵用,大家一起等,然后再一起调,还是会再次产生死锁。对于这个问题,我提交了一个Patch对其进行优化,具体内容可以参考 #71 《oslo.db中的死锁重试机制优化》的详细分析。
上文已经提到,造成死锁的根本原因实际上是在一个事务中,更新2个表的时候的顺序不一致。在并发调用的时候,产生了死锁。Python的代码是按顺序更新的(先更新event内容,再更新action),但是为什么SQLAlchemy产生的SQL是乱序的呢?
通过阅读SQLAlchemy的源码,最终找到了答案。先说结论:Session中的操作顺序,由UnitOfWork机制决定最终的调用顺序,如果没有依赖关系,最终执行顺序是不稳定的。
SQLAlchemy在进行数据刷新的时候,会有一个flush的过程(实现见lib/sqlalchemy/orm/session.py#def flush,这个过程会将所有的object的变化,刷新到数据库中。例如,会将插入、修改、删除,转换为INSERT、UPDATE、DELETE等操作。而刷新执行的顺序,是通过Session的”UNIT of Worker”依赖机制保证的。
我们可以从有SQLalchemy作者写的一篇关于其架构的文章《SQLAlchemy》中看到一些关于Session相关的数据结构:
Session维护着如上图所示的结构,在每次刷新的时候,会将object的变动刷新到数据库中。如作者所说说,flush这个函数可能是 SQLAlchemy最复杂的函数。
我们先看看来自作者的介绍:
The job of the unit of work is to move all of the pending state present in a particular Session out to the database, emptying out the new, dirty, and deleted collections maintained by the Session. Once completed, the in-memory state of the Session and what’s present in the current transaction match. The primary challenge is to determine the correct series of persistence steps, and then to perform them in the correct order.
UOW的工作主要是将session维护的new、dirty、deleted的集合清掉并落入数据库中。主要挑战就是决定正确的持久化步骤和顺序。我们看到了关键的地方,排序!
从这篇文章中,我们了解到,其实对于UOW来说,共有两级排序:
1) 第一级排序,是针对于多个表(class)之前的排序,依赖信息从表之间的关系获取,例如文章中所举的User和Address的例子,需要在user插入后,有了主键,然后再去更新。
2)第二季排序,是针对于一个表(class)之中操作的排序,例如文章中所举的,前一个插入的user依赖后一个user。
然而,无论是哪个排序,如果表和表之间在SQLAlchemy定义模型的时候,并没有指定其顺序,那么便没有依赖关系,也便意味着,顺序是不稳定的。
在我们出现的问题中,action和action_event在model定义的代码中,并未指定action和event之前的关系,因此,SQLAlchemy分析依赖的时候,只是将这两个表当做独立的2个表。
为了证明我们的分析,我们在SQLAlchemy打印一些日志来记录依赖关系和最终执行的结果,代码见lib/sqlalchemy/ormunitofwork.py,取消掉这些注释即可。
dependencies: set([(SaveUpdateAll(Mapper|InstanceActionEvent|instance_actions_events), DeleteAll(Mapper|InstanceActionEvent|instance_actions_events)), (SaveUpdateAll(Mapper|InstanceAction|instance_actions), DeleteAll(Mapper|InstanceAction|instance_actions))])
cycles: set([])
sort: [SaveUpdateAll(Mapper|InstanceAction|instance_actions), SaveUpdateAll(Mapper|InstanceActionEvent|instance_actions_events), DeleteAll(Mapper|InstanceActionEvent|instance_actions_events), DeleteAll(Mapper|InstanceAction|instance_actions)]
COUNT OF POSTSORT ACTIONS 4
上面共4行信息,我们需要的是dependencies信息和sort信息,从依赖信息我们可以看到,我们进行的这个事务仅有2组依赖,分别是action和event_action的缓存入库先于缓存清空,而action和event_action之间是没有依赖关系的。所以,最终生成的sort列表,其实是无法保证稳定性的。
所以,才会出现我们本文所出的问题,一会先刷新action,一会先刷新action_event。然而,对于这种问题并不是无解,我们只需要在这两个表里加入relationship,使他们有依赖就可以了。如果确实没有什么关联,那我们就需要思考把更新拆分到更小的事务中了,就像MySQL官网说的那样:Keep transactions small and short in duration to make them less prone to collision。
TL;DR。写完这篇文章发现,有点太长了,不想细看的看看总结吧,哈哈。
L版本特性列表:链接
https://blueprints.launchpad.net/nova/+spec/request-spec-object
https://blueprints.launchpad.net/nova/+spec/request-spec-object-mitaka
根据M版本特性优先级的信息,我们看到M版本规划的工作还是围绕着request spec和object来展开的。在M版本特性列表中,我们可以看到确实重点还是在request spec的完善,其中虽然Inventory的拆分在这个版本提出,但是最终没有被完全实现,而是拖延到了N版本才完成。
Link: bp/resource-classes
原来我们增加某种资源的时候,都需要给instance的object增加filed,每一次改变,都要刷一下db shema,或是增加filed或是增加整个table,也就是说新增一种类型的支持,管理员就要把数据库也升级,这意味着会有业务中断,很不合理。
随着资源种类变多,这种方式有点臃肿,也不是很优雅,在紧接着的generic resource pool的bp中,也亟需把资源抽象出来了。
因此,在M版本新增了一个Resource Class的object,用于记录虚拟机资源的类型,无论增加新的独立资源类型还是共享资源类型,都不需要再对db结构刷新了。
这个BP可以说是generic-resource-pools这个bp的基础,只有通过这种通用的方式将资源类型表征出来,才有可能将所有的资源都抽象出来。
根据N版本特性优先级来看,首次提到了要把Placement相关的东西独立出来,并且把cell数据库中的compute node的数据迁移到API DB的inventory和allocation中。因此,这个版本可以看做是Placement的元年。而从N版本特性列表中,我们看到了Placement所依赖的那些基础结构和模型均已在这个版本支持了。
Link: bp/generic-resource-pools
这个是一个非常重要的BP,可以认为是Placement组件的初始BP,原先,compute node的理念,把计算的资源强行绑到某一个node上了,而实际上,存在着一些诸如共享资源的情况,可能多个计算节点共享着某些资源。
所以,Nova期望能够定义一种“通用的资源”模型。
我们可以看到几个在Placement的重要概念都有在这个BP提到。
Resource Provider:名释其意,资源提供者,结构比较简单,UUID和这个RP对应的一些基本信息,比如name之类的,资源提供的具体资源的存量和消耗量,通过UUID关联到其他表中。
Inventory:存量,用来记录资源的总量,并且记录着资源可分配的最大、最小、步长等信息。每个RP(比如计算节点)的每种资源(比如vcpu、内存等)都占一行。
Allocation:已分配量,用于记录某个RP(如计算节点)被某个消耗者(如虚拟机)
Link: bp/resource-providers
Resource Provider模型主要的目的就是为了把compute node这个模型替换掉。
原来Nova假设所有的资源都是通过单个计算节点提供的,所以所有的资源都通过compute node来记录,后面发现对于共享资源的这种场景比较棘手了。
所以,提出了通过RP这个新的模型准确的记录资源情况。
Link: bp/compute-node-inventory-newton
首次提出在mitaka版本,直到Newton版本才实现。
之前,nova是用compute node来记录计算节点上的资源,即一种资源都会有一列,比如vcpus、memory_mb、local_gb。
这样资源对应的所有资源的总量、已使用量、剩余量每个都占一列,所有的都揉在一起。
在我们增加资源类型的时候,需要加很多列,而且在进行资源更新的时候,都需要刷新compute node,而且这些更新是有锁的,效率很低。
这个BP先把总量信息Inventory抽出来了,其中有个字段是resource type,就是在resource class中实现的资源类型。
这样一来,增加一种新的资源类型时,数据库结构不会发生变化,只是增加了一条resource type不同的记录。
疑问:inventory在何时会被刷新,何时会被访问?
Link: bp/resource-providers-allocations
与Inventory类似,allocation也是compute node拆出来的,用来记录某个RP被某个消费者消费的记录。
疑问:allocation在何时会被刷新,何时会被访问?
可以看到,在Newton版本,Placement所依赖的基本能力(如Resource Provider、Inventory、Allocation等)均已经被支持了。
根据O版本特性优先级,我们可以看到,调度统一放在了API Cell中的Conductor进行,而特意的将Resource Provider作为一个重要的环节,其中包括了调度的Placement接入、Aggregate支持以及自定义资源类型。从O版本特性列表也可以看出,Ocata版本主要工作也是集中在Cell V2架构的调度能力及Resource Provider相关能力机制的补齐。
Link: bp/cells-scheduling-interaction
从O版本后,调度会在API Cell进行,这个BP在conductor中增加了schedule_and_build_instances方法,通过这个方法向指定的cell进行虚拟机创建。将API Cell和计算Cell分离,将调度层与计算层解耦。
Link: bp/custom-resource-classes
为Placement增加自定义Resource class的能力,之前仅支持基本的vCPU、内存、磁盘等基本的资源类型,这个BP增加了让用户自定义资源类型的能力,比如增加自定义的FPGA、裸机调度的能力。
增加了一个resource classes表,用于记录自定义的资源。并且增加了基本的CRUD接口,使用户可以增加、删除、修改、获取指定的resource class。
Link: bp/generic-resource-pools-ocata
继续支持generic resource provider的基本能力,比如aggregate等。使用新的Placement及Resource tracker来对资源进行刷新和调度。
Link: bp/resource-providers-get-by-request
用户通过请求可以获取一个满足用户资源需求的Resource Provider的列表。
Link: bp/resource-providers-scheduler-db-filters
看着有点标题党的BP,把Filter全搞到DB了?实际上,只是在Filter&Weight之前,加入了Placement请求的流程,相当于先通过Placement进行过滤,然后再进行后续的Filter和Weight。
这个BP标志着我们的Placement真正开始工作了,在每次的虚拟机创建的流程中,都增加了和Placement交互的流程,在大规模部署的环境中,我们不用一个一个host来过滤了,先通过Placement搞一把,会把不满足条件的节点过滤掉很多,大幅度提升调度的性能。
Link: bp/placement-api-policy-authz
为Placement服务增加Policy机制。
P版本特性列表:链接
Link: bp/placement-allocation-requests
新增了一个allocation_candidates的接口,通过这个接口,支持用户请求资源,期待返回满足用户需求的resource provider。原来,在O版本的实现,仅支持返回单个的resource provider,这样对于共享及嵌套的rp有些问题,现在把这个接口增加后,使得用户可以进行更复杂的过滤。
Link: bp/placement-claims
将资源claim的过程从compute移动到scheduler中,主要有2个考虑:其一是减少从调度成功到claim时的时间,从而降低重试的可能,因为从scheduler拿到这个可选的请求,再到compute node进行claim,消耗时间较长,在资源较为紧张的时候,并发请求有可能在scheduler是满足的,但是真正到compute时,已经被其他人消耗了;其二是为了适配Cell V2的架构,Cell V2中,子Cell不应该反向去调用scheduler,比如在重试的时候。在Q版本,会有一个“调度备选节点列表”,配合这个,减少调度重试的可能。
Link: bp/custom-resource-classes-in-flavors
允许在Flavor中定义自定义的resource class,并在调度的流程中增加处理这个extra specs的能力。形式类似于resources:$CUSTOM_RESOURCE_CLASS=$N
或者resources:$STANDARD_RESOURCE_CLASS=0
。
Link: bp/custom-resource-classes-pike
继续完善自定义资源类型的能力。
Link: bp/placement-project-user
为Placement增加project和user的过滤能力,仅获取某个租户或者用户的资源情况。bp完成后,用户可以直接通过project和user去拿资源可用情况了,底层会直接从数据库过滤,而不是像现在的,全拿到然后再从所有结果中去统计。
Link: bp/placement-put-resource-class
允许用户直接通过PUT来创建不存在的resource class,而不是先POST(创建)再PUT(更新)。
我们知道Nova目前正在慢慢地演进到Cell V2架构,Cell V2架构中,很重要的一个变化就是数据库的拆分,清晰的划分了数据库的职能,从而有具备横向扩展的能力。顶层数据库(nova_api)用来存储全局数据,而Cell中的数据库(nova_cellX)仅存储计算节点相关的数据。比如,创建虚拟机的全局数据,比如Flavor、Keypair之类的数据,放在上层的nova_api数据库中,而虚拟机本身的信息,比如某个虚拟机的信息,放在了子Cell中。
这样的架构另一个好处是Cell很轻松的可以实现扩展,从而提升虚拟机数量的规模。然而,这引入了一个问题,就是没有一个地方存储着全量虚拟机的数据了。当我们需要一些全局的虚拟机数据查询时(比如查询全量虚拟机列表)就比较棘手了。
其实这样的架构在目前互联网业务中十分常见,随着业务量和历史数据的增长,很多业务都需要进行分表分库,切分的目的主要有2个,一是单个数据库的存储空间已经不足以支撑庞大的数据量,另外一个是单个数据库所能承载的连接数或者并发数不足以满足逐渐飙升的请求。一般来说,数据库的分库为垂直分库和水平分库。
垂直分库。
一般按照功能划分,每个分库的功能不同。把不同功能查询或写入的负载均分到独立功能库中。例如,我们将一些基础信息独立成一个库,详细信息独立成一个库,这种按照功能的划分,将负载均衡,只需要基础信息的去访问基础库,需要详细信息的时候,再去查详细信息的库。
在Cell V2的架构中,我们可以将nova_api和nova_cellX的数据库划分看成是垂直划分,对于nova_api库来说,只用关心上层全局数据的存储处理,而对于nova_cell库来说需要关心的是每个子cell里面数据的存储处理。
水平分库。
一般通过某种方法把数据打散到不同的库中,每个库的表结构是相同的。例如,我们根据用户ID进行分库,可以通过映射表、取余、Hash的方式来确定某个用户的请求到底落到哪个数据库去查。
在Cell V2的架构中,每个Cell数据的划分,就可以看做是水平分库了,虚拟机按照一定的“规则”,落到了不同的Cell中。
一般的业务演进,一般是先进行垂直分表分库,然后当用户或者数据规模达到一定程度后,再通过水平分库提升规模。就像OpenStack Nova一样,最开始所有的数据都聚集在一个叫Nova的库里,然后拆分出nova_api和nova,最后再将nova_cell拆出来,并根据虚拟机ID和Cell进行mapping,从而完成水平分库。
在Cell V2的场景下,对于单个虚拟机来说基本没什么变化,无非就是多了一个映射查询的步骤:先查找虚拟机所对应的Cell数据库,然后对这个数据库进行操作即可。
然而,由于水平切分导致每个Cell都丢失了“全局视角”,例如之前我们进行虚拟机列表查询时,在原来只需要在一个数据库查询,现在需要在多个数据库查询,尤其是需要指定一些全局参数进行查询时,比如limit、marker、sort等参数一加上,就更恶心了。
这个问题,在多Cell场景一直没有得到很好的解决,只是在各个Cell里面搜集各个cell排序好的数据,然后append到结果里面,也并没有进行最终排序。呈现给用户来看,就是数据是乱序的。
直到Q版本,Dan Smith大神提了一系列的instance-list Patch),初步解决了这个问题。
我们先看看他的实现,这一系列的Patch最关键的实现在instance_list.py中,大致的撇一眼整体的实现,全局/局部marker、并行搜集跨Cell数据、heap归并排序,相信从这几个关键词中你已经猜到了实现的大概逻辑。下面,我们先整体介绍一下整体的实现逻辑,然后再对实现细节做详细的解释。
我们举个例子,来看看在跨cell场景下,如果获取一个虚拟机的列表
假设我们有2个cell,里面有编号为i0~i5的6个虚拟机,从i0到i5,按顺序依次创建。当我们查询全量虚拟机时,按照创建时间逆序,我们期待得到的结果是i5 i4 i3 i2 i1 i0
。再传递limit=2/sort=orderby(created_at,desc)/marker(i5)之后,处理的过程如下:
我们首先需要在各个Cell中查找,i5
是否存在。对于虚拟机来说,比较简单,在nova_api数据库中存在instance_mappings表,这个表里面记录了虚拟机和cell的映射关系,如果marker在cell中,我们直接可以从mapping表中找到它及其对应的cell。
我们的例子中,我们找到了位于cell2中的i5
。
参考代码:nova/compute/instance_list.py#L134,L142
具体获取marker时,是先在instance_mapping中查找了marker所在的cell,然后,在target_cell拿到了marker的信息,参考代码nova/compute/instance_list.py#L66,L88。
在第一步中,我们拿到了i5的信息,由于排序是按照created_at,逆序,那我们查询local marker的时候,只需要一条SQL就可以拿到Cell中满足条件的local marker:1
SELECT * FROM instances ORDER BY created_at DESC limit 1 where created_at <= i5.created_at
然后,我们再根据这个local marker就可以拿到满足条件的虚拟机列表了。当然,我们需要各取limit个,因为也许最终满足条件的都在一个cell中,所以,我们取得的虚拟机列表中虚拟机的个数应该按照全局limit来获取。
值得注意的是,local marker如果不是global marker的话,我们需要把local marker也算在满足条件的列表中,因为全局来看,这个marker也是满足用户条件的。如果local marker也刚好就是global marker,那这个marker就不用管了。
参考代码nova/compute/instance_list.py#L166,L212,先找到满足条件的第一个local marker,然后就按照之前的流程,获取marker之后的记录就可以了。
在步骤2中,我们会得到2个列表,分别是从cell1和cell2中拿到的数据。这两个列表是有序的,但是当我们合并后,需要进行全局重排序。这样,我们就拿到了有序的列表。
参考代码nova/compute/instance_list.py#L228,L234,可以看到,由于每个cell拿到的数据都是有序的,因此,最终排序的时候,也是用了heapq这个数据结构,来高效的完成有序列表的合并排序。
最后,我们在第三步的结果上,进行limit操作就好了。在合并的过程中,对limit进行检查,最终,完成limit且排好序的结果。
Instance list给我们示范了一个跨Cell场景下列表的实现,主要有2个地方很有亮点,一个是并行的查询各个Cell的数据,一个是最终排序选择了heapq作为排序的结构,在保证分页正常的情况下,也兼顾了性能。
比较残忍的是,随着跨Cell迁移的支持,几乎后续所有和子Cell数据相关的列表查询,都需要进行跨Cell的支持,比如migration、instance action。
架构很丰满,实现很骨感。不多说了,滚去写cross cell support的patch了。:)
]]>这篇文章主要讲了在过去几个版本中,OpenStack社区对于Nova调度及Placement服务相关工作的更新进展。我也会着重介绍一些我们在Q版本中主要处理的几个BP,同时也介绍了未来重点工作的路标,我们会在未来的几个release中完成它们。
Placement API已经在OpenStack的N版本成为一个独立的API endpoint。
Placement API将存量记录(tracking inventory)、资源消耗(resource consumption)、资源的分组和共享(grouping and sharing of resources)以及表示资源所具能力字符串标签(string capability tags, traits)这些数据暴露出来。
从那之后,社区持续地改进API,并且在Nova中更进一步地进行集成。
在Newton版本,主要是让nova-compute正确地对本地的资源进行盘点,然后把这些inventory记录送到Placement API中。
在Ocata版本,我们开始将nova-scheduler服务与Placement API进行集成。我们在scheduler进行了一些修改,使用Placement API进行满足一些基本资源请求条件的计算节点过滤。我们也添加了aggregates,来提供resource provider的分组机制。
在Pike版本,我们主要将资源claim这个步骤从nova-compute移动到nova-scheduler中。做这个事主要有2个原因:一个是性能/扩展以及和Cells V2架构适配。在”候选主机列表及cell中重试”一节中,进行了详细介绍。
在丹佛的PTG中,Nova社区团队决定了在调度和资源placment功能中3个主要的工作:
应该指出的是,我们理解在这个领域仍然有许多许多的新功能需求,一些已经在我们的“雷达”中很多年了。我们承认当一些管理员或者一些潜在的用户看到一些长时间存在的问题或者工作并没有出现在Queens版本的高优先级列表中,会有点沮丧。然而,core团队实际能够review的东西是有限的,所以我们必须做这些决策。当然,我们也非常欢迎大家在PTG和邮件列表里来讨论这些决策。
另外一个应该指出的是,尽管在Q版本的scheduler和resource placement领域中,仅有这3个高优先级工作,但这并不意味着其他的工作不能被review或者推进。这个仅代表core团队将重点review这些领域的patch。
我们在Q版本第一优先级需要处理的是做与placement API相关的move操作(比如migrate、resize、evacuate、unshelve等)的收尾及测试用例的全覆盖。
在Pike版本所剩时间不多时,Balasz Gibizer、Dan Smith和Matt Riedemann发现了一系列问题:在做Nova支持的各种各样的move操作时,资源应该如何在Placement API中记录。在Pike版本的时候,我们开始在nova-scheduler服务里进行资源的claim。很显然我们也需要在move操作的时候进行claim处理。在最初版本的实现,在虚拟机迁移的过程中,我们为虚拟机创建了一个成对的allocation,源节点和目的节点的资源都会占用一条allocation记录。这样做,可以工作,但是这个方案明显有弊端,尤其是在我们在同节点进行resize的操作。
这产生了一系列关于在迁移时产生错误的或者丢了一些allocation记录的问题,我们需要在resource tracker和compute manger中,塞了很多不优雅的代码,来处理在滚动升级中新的conductor和scheduler服务的兼容,并且旧的计算节点也写入了不正确的记录。
Dan Smith也提出了一个解决在迁移操作中记录allocation问题的方法migration-allocations,但是因为时间问题,我们没办法在Pike版本实现。
在Queens版本,我们需要优选Dan的解决方案,即在迁移之前,在allocation记录中,将源节点的UUID替换为迁移object自己的UUID。这样允许目的节点使用虚拟机的UUID来占用allocation,一旦迁移成功,我们便可仅仅删除被migration UUID消费的allocation就行了。再也不用受双倍的allcations困扰了。
第二优先级的事情是,我们应该在Cells V2部署中支持创建虚拟机请求的重试能力。
上面,我已经说过了,将资源claim的过程从compute移动到scheduler中,有两个原因,让我们来仔细讨论一下。
首先,现在的版本存在一个问题:2个scheduler进程为2个虚拟机选择同一个host时,当启动虚拟机先第一个host启动完成时,消耗了host的最后一点资源。然后,不幸的第2个启动进程,必须进行重新调度流程。这个流程有点太重了,sheduler必须通过通过RPC来为虚拟机获取一个新的目的节点,然后许多状态都需要通过request spec来传递。补充一下,并没有什么能够确保新的节点就一定可用,可能同样的命运将降临,再一次触发了重试。
在nova-scheduler代替目的计算节点来进行资源claim操作,意味着我们可以显著的减少在compute节点进行claim的时间和复杂性(造成重试操作的主要原因是在计算节点claim资源的竞争和竞态条件)
现在我们尝试在scheduler服务选择目标主机的时候,进行资源的claim操作。如果Placment API返回200 OK,我们就知道虚拟机已经在目的节点已经占用了这个资源,唯一可能造成重试的操作就是某些不正常的主机失败,即不是常见的失败原因。如果Placement API返回409冲突,我们可以从返回的error中看到失败原因,到底是因为并发刷新失败了,还是说目的节点确实没有足够的空间来容纳虚拟机。
如果另外的进程在目标主机完成资源claim的时间介于虚拟机调度选择和尝试claim资源之间,我们会简单的重试(在scheduler代码的小循环)尝试在目的主机claim资源。如果目的主机资源耗尽了,scheduler会选择另外的目的主机。我们完成这些时,不会启动的请求发送到目标的compute主机。
我们将资源的claim移动到scheduler的第二个原因,是因为Cells V2架构。再次说明,Cells V2架构移除了独立分离又略显奇葩的Cells V1旧代码。单单使用的Cells V2 API控制面,意味着能够更简单也更容易地进行代码维护。
然而,Cells V2设计架构的一个原则是一个启动(或者移动)虚拟机请求会获取到目标的cell,他没有向上调用的能力与API层进行通信。这对于我们目前的重试机制来说是一个问题。当前的重试机制依靠失败的计算节点来初始化资源的claim,并且能够反向调用scheduler,来找到另外的host进行虚拟机的启动。
Ed Leafe正在Quees版本努力,让scheduler从API/Scheduler传递一系列备选主机和allocation到目标的cell。这个备选主机和allocation信息,将会被cell的conducotr用来去那些备选的目的节点去重试,虚拟机资源的claim依靠备选host,无需访问上层的API/Scheduler。
第三优先级的事情是我们称之为”嵌套的resource providers”的东西。
例如,NUMA Cells和包含它的主机,SR-IOV网卡功能和包含它的主机,物理GPU组和包含它的主机。
让我们举个2个计算节点的例子,2个节点每个都含有2个SR-IOV网卡。第一个计算节点每个网卡都有8个虚拟机网卡,第二个计算节点其中的一个物理网卡被标记为直通的(意味着用户能够全面掌控)另外一个网卡则被指为8个虚拟机网卡,与第一个计算节点类似。
目前,Placement API无法理解父子provider这层关系。嵌套的resource provider这个spec和patch让Placement服务能够感知到这些,也允许用户区分子资源provider的父资源provider的UUID。
嵌套的resource provider开启了一系列的功能,包括PCI设备、高级网络、NUMA等的支持。正因如此,我们将共享的resource provider在Q版本放在了相对来说不重要的位置,然后更专注实现基本的嵌套resource provider,至少支持SR-IOV物理功能和虚拟功能的关系。
除了上面的优先级事项,我们也将投入精力去做其他的一些事物。尽管review将专注在上述的优先级高的事物中,我们也将在下述的几个方面尽可能地进行review。
这个是从Pike版本开始出现需要完成的工作。Placement API现在支持traits列表,简单的tags字符串来描述一个resource provider的能力。
然而,一些地方仍然需要编码完成。
如上面提到的,virt driver需要开始上报traits信息给计算节点的resource provider。然而,ironic driver有点不同,因为他处理了多个计算节点的resource provider记录(有一个resource provider记录着部署的每个Ironic裸机节点)
Jhon Garbutt在主要负责Ironic API支持在virt driver中上报traits。
Chris Dent提出了在Placement API中的一些资源endpoints增加”Last-Modified”和其他的HTTP头部。确保在cahcing代理下的正确行为非常重要,并且完成这项工作的工作量似乎是可以控制的。
这个spec实际上是为了move操作的清理而开启的。Chris提出允许PORST /allocations调用(我们目前支持PUT /allocations/{consumer_uuid}调用)来支持在一个请求中写入多个consumers的多条allocation记录。这个将允许我们完成allocation从instance到migrations UUID的转换,这个是Dan Smith做move操作资源跟踪方案的一部分。
尽管这个不像从Citrix的Jianghua Wang提出的spec来完全实现vGPU,在Queens版本,我们仍将尝试至少完成vGPU资源的基本支持。
基础的支持意味着可能不会支持多GPU类型或者pGPU池(换句话说,仅支持每个计算节点的VGPU有一个单一的inventory记录)。
Eric Fried和我正在讨论一个generic device manager,将会在已存在的nova/pci模块中替换很多代码。我们可能最早在Rocky版本完成。
尽管嵌套的resource provider是为NUMA拓扑设计的,实际上,想要能够通过Placment API同等功能的把NUMATopologyFilter在Nova scheduler取代,仍然还有很长时间,可能在Rokcy版本吧。
在Nova支持NUMA的实现与支持大页内存、CPU pining、模拟IO线程pinning甚至是PCI设备管理(比如PCI设备的NUMA亲和性)是强耦合的。
似乎在可以看到的将来,NUMATopologyFilter仍然在Nova Scheduler保留,作为一个复杂自定义的调度failer/weigher,我们会慢慢的修改virt driver接口和resource tracker在nova-compute节点向Placement API上报NUMA cells信息作为resource provider。渐渐地通过查询Plament database来替换一些NUMATopologyFilter的功能。
Placement API允许resource provider通过aggregate关联来和其他provider来共享资源。这些reousrce provider我们称之为”shared resource providers”,尽管使用”sharing resource providers”更能合适地表达其目标。
我们需要为共享存储和路由网络IP池的用力完成和增加一些功能测试,确保资源上报和跟踪正确的完成。
]]>虚拟机冷迁移由于当用户想把虚拟机从一个计算节点移动到其他节点。主要涉及的命令如下:1
2 nova migrate server_id
nova resize-confirm server_id
看到后是不是觉得有点奇怪为啥migrate之后,还要resize-confirm?resize操作其实和migrate操作比较类似,不同的是迁移前后的flavor不一样。一般情况下resize的场景是,对虚拟机进行扩容,把flavor调大之类的。所以,在代码级别,nova也将两个流程合一了。migrate就是一个没有flavor变化的resize。
下图是虚拟机冷迁移时,涉及的组件交互:
我们可以看到,在迁移时,主要流程包括调度、迁移准备、迁移、完成迁移。
具体细节,包括迁移的状态变化,如下图所示:
Nova是OpenStack中处理计算业务(虚拟机、裸机、容器)的组件,整体的虚拟机创建流程自然是学习和熟悉Nova组件的第一步。本篇文章主要基于OpenStack Pike版本,基于最新的Cell v2架构部署为例,来介绍虚拟机的创建流程,并分析了Pike等最近几个版本中,虚拟机创建流程的关键变化。
上图是虚拟机创建流程的整体流程,可以看到整体虚拟机创建流程一次经过了API、Conductor、Scheduler、Placement、Compute等主要服务,下面我们逐步介绍下虚拟机创建时,这些服务做的一些事情以及在Pike版本新引入的部分:
在OpenStack的组件中,基本每个组件都会有一个API服务,对于Nova来说API服务主要的作用就是接收由用户通过Client或者一些其他REST请求工具(比如 curl、postman)发送的请求。一般来说会包含一些虚拟机创建的参数,比如虚拟机的规格、可用域之类的信息。
在虚拟机创建的流程中,API就是Nova的入口,当API接收到请求后,主要会处理一些关于参数校验、配额检测等事务。
例如,我们指定镜像和规格来创建一个虚拟机时,通常会使用:1
nova --debug boot --image 81e58b1a-4732-4255-b4f8-c844430485d2 --flavor 1 yikun
我们通过--debug
来开启debug模式,来看看命令行究竟做了什么事,可以从回显中,看到一个关键的信息:
curl -g -i -X POST http://xxx.xxx.xxx.xxx/compute/v2.1/servers -H “Accept: application/json” -H “User-Agent: python-novaclient” -H “OpenStack-API-Version: compute 2.53” -H “X-OpenStack-Nova-API-Version: 2.53” -H “X-Auth-Token: $token” -H “Content-Type: application/json” -d ‘{“server”: {“name”: “yikun”, “imageRef”: “81e58b1a-4732-4255-b4f8-c844430485d2”, “flavorRef”: “1”, “max_count”: 1, “min_count”: 1, “networks”: “auto”}}’
我们可以看到虚拟机创建时,传入了一些诸如虚拟机名称、镜像、规格、个数、网络等基本信息。在API中,首先就会对这些参数进行校验,比如镜像ID是否合法、网络是否正确等。
值得一提的是,在Pike版本的虚拟机创建开始时,对配额检测进行了优化。
我先看看之前的实现,在之前版本的Nova中,Quota检测过程相对来说比较复杂,首先会进行reserve操作,对资源进行预占,然后预占成功,并且创建成功后,最终会进行commit操作。然而,为了保证在并发的场景下,不会对超过用户配额(这都是钱啊!),因此在reserve和commit进行资源更新的时候都会quota相关的数据表的用户相关行加把锁,也就是说更新quota记录的时候,一个用户去更新时,其他用户再想刷新只能等着,直到前一个用户完成数据库记录刷新为止,这样就大大降低的效率,并发的性能也就不是很客观了。
另外,由于需要对cell v2进行支持,目前所有的quota表均已移动到API的数据库了可以参考BPCellsV2 - Move quota tables to API database。Cell V2的设计思想是,由API、Super Conductor去访问上层的全局数据库(nova_api数据库),而底下的cell中的组件,只需要关心cell中的逻辑即可。因此,为了彻底的解耦,让cell中的compute无需再访问api数据库进行诸如commit操作,在Pike版本,社区对quota机制进行了优化,详情可以参考Count resources to check quota in API for cells这个BP。
因此Pike版本之后,配额检测变成了这样:
每次检测的逻辑都调用相同的函数,具体逻辑如下图所示:
Super Conductor在创建虚拟机的流程其实和之前差不多,选个合适的节点(调度),然后,写入虚拟机相关的记录,然后,投递消息到选定的Compute节点进行虚拟机的创建。
在Cell v2场景,虚拟机的创建记录已经需要写入的子cell中,因此,conductor需要做的事,包括一下几个步骤:
完成这些操作时,需要牵扯到3个关键的数据结构,我们来简单的看一下:
与Cell v1不太相同,在目前的设计中,认为scheduler能看到的应该是底下能够提供资源的具体的所有的Resource Provider(对于计算资源来说,就是所有的计算节点),而不是整个cell,也就是说所有cell中的资源scheduler都可以看到,而子cell就负责创建就好了。因此,在super conductor中,需要做一些transfer的事情,这样也就不必在像cell v1那样,在子cell里还得搞个scheduler去做调度。
刚才我们在conductor中,已经介绍了,在选择具体哪个节点来创建虚拟机时,调用了Scheduler的select_destination方法,在之前的版本的调度中,就是OpenStack最经典的Filter&Weight的调度,已经有大量的资料介绍过具体的实现和用法。可以参考官方文档Filter Scheduler。
在Pike版本中,在调度这部分还是做了比较大的调度,主要就是2个相关变动。
通过Placement获取可用的备选资源,参考Placement Allocation Requests的实现。
在Ocata版本时,Resource Providers - Scheduler Filters in DB这个BP就已经在调度前加了一步,获取备选节点。从BP的标题就可以看出,设计者想通过Placement服务提供的新的一套机制,来做过滤。原因是之前的调度需要在scheduler维护每一个compute节点的hoststate信息,然后调度的时候,再一个个去查,这太低效了,尤其是在计算节点数目比较多的时候。因此,增加了一个“预过滤”的流程,通过向Placement查询,Placement服务直接通过SQL去查一把,把满足条件(比如CPU充足、RAM充足等)先获取到。
而原来获取备选节点的时候,只支持获取单一的Resource Provider,这个BP增强了获取备选资源的能力,用于后续支持更复杂的请求,比如共享资源、嵌套资源的Provider查询。后面,Placement还会陆续支持更多的请求,比如对一些非存量不可计数的资源的支持。这样留给后面Filter&Weight的压力就小一些了,再往后,会不会完全取代Filter呢?我想,现有的各种过滤都可以通过Placement支持后,完全有可能的。
Scheduler通过Placement来claim资源。参考Scheduler claiming resources to the Placement API的实现。
在最早的时候,claim资源是由compute来做的,现在相当于提前到scheduler去搞了。有什么好处呢?我们先看看原来的问题:
调度时刻和真正的去compute节点去claim资源的时刻之间是由一段时间的,在资源不是那么充足的环境,就会造成在scheduler调度的时候,资源还没刷新,所以调度时候成功了,但是真正下来的时候,才发现compute实际已经没有资源了,然后又“跨越半个地球”去做重调度,无形地增加了系统的负载。
而且增加了创建的时长(哦,哪怕创建失败呢?),你想想,用户创了那么久的虚拟机,最后你告诉我调度失败了,用户不太能忍。
所以这个BP就把Claim资源放在调度处了,我上一个调度请求处理完,马上就告诉placement,这资源老子用了,其他人不要动了。OK,世界终于清净了,能拿到资源的拿到了,拿不到资源的马上也知道自己拿不到了,大大增强了调度的用户体验。
恩,在调度的时候,已经介绍过这个服务了,在虚拟机创建的流程中,比较常用的接口就是获取备选资源和claim资源。
Placement目标很宏伟,大致的作用就是:资源我来管,要资源问我要,用了资源告诉我。后面准备用一篇文章整体介绍一下Placement。(yep,这个Flag我立下了,会写的)
好吧,到最后一个服务了,Compute。这个里面依旧还是做那么几件事,挂卷,挂网卡,调driver的接口启动一下虚拟机。至此,我们可爱的虚拟机就起来了。
整体的看一下,其实在Pike版本,Nova还是有很多的变动。真的是一个版本过去了,创建虚拟机的流程已经面目全非了。
从P版本的虚拟机创建流程来看,主要的优化集中在基于Cell V2架构下的多cell支持、调度的优化、Quota的优化,而后续的发展,目前也是集中在Placement各种资源的支持以及在Cell v2场景的诸如基本流程、调度等的优化。
]]>译注:本篇文章为作者介绍Cinder AA方案的文章,作者是gorka,是实现cinder AA BP的core,文章介绍了这哥们实现AA时的记录,算是对方案的一种解释以及设计思路的总结,核心思想为以下几点:
cluster
的配置项,作为集群,标记这个节点属于某个集群;原文链接:Simpler Road to Cinder Active-Active
上一周,我发了一篇文,来介绍Cinder AA配置方案,可是,让我很受伤的是,觉得那个方案有点太复杂了,所以呢,这一波又搞了个简单的方案。
我确实很喜欢我上周发布的允许Cinder使用AA HA配置的方案,不过想起来,确实有些复杂了,没必要为了一点点的益处,就把那么复杂的机制加到组件里。(比如恢复排队任务)
这个方案从决绝到接受,没有花费我太多时间,毕竟上个方案确实留下了一些复杂,所以必须要一个更简单的方案,所以呢,我相信这一波方案是一个合理的选择。
我准备用上篇博客同样的格式,来对同样的问题给出解决方案,这样对于读上篇博客的人来说看起来比较熟悉。尽管在这我没有太多的时间来搞patch和流程图,我会在Cinder Midcycle Sprint的时候把他们准备好。
我们应该如何在一个cluster中的不同node去做任务调度呢?
在任务调度的时候,我们仍然不想让API或者Scheduler节点太多的考虑有多少volume节点或者说操心具体集群中哪一个节点时可用的。我们将还是用原来的方法——主题队列,唯一的不同在于,我们改变了这些队列的主题,从原来的host@backend变为了cluster@backend。
cluster将成为一个新的配置——同一集群中的所有节点都使用相同的配置。如果没有配置的话,默认会使用host作为值。同时,在cinder部署时,任何一个节点的host应该是唯一的。
将host的值作为cluster的默认值有很多好处,非AA配置可以将服务视为之前的host@backend,如果一个节点或者说原来的主备配置的节点,想要变为AA模式,只需要把后面加进来的节点,把cluster都改成和第一个host的值就行了,这样就可以保证服务可以不挂掉。这样做虽然不是那么干脆利落,但这个可以帮助管理员在可以down机的时候,再去做整改。
为了进行清理,我们需要在所有的资源表(如卷、备份、快照等)增加一个新的字段。我们称其为“worker”,它可以有3个不同的值:
或许,我们不需要cluester@backend这个值也可以工作的很好,不过我还是觉得应该记录现在处于什么状态了
我们也用到了“previous_status”这个字段,就像我们对in-used卷做备份操作那样。
对于已经存在的资源,正常的工作流程像下面一样:
看起来不错,但我们如何去做清理呢?其实和目前的实现差不多,仅有一点小小的改动。
当节点启动时,搜寻所有worker为自己所属的host@backend的资源,然后对这个worker filed做compare-and-swap置空,确保没有其他节点来操作这个资源,也保证仅做了我们需要的改动。
然后对于一些资源的操作(比如deleting)我们设置worker字段为cluser@backend,然后RPC调用。这样,我们在所有集群的节点做了分发。
还是看起来不错吧?但是我们如何处理那些没有恢复如初,或者那些无法failover的备节点呢?这个case会通过Scheduler来处理,我们会在后面介绍。
另外的选择是,我们不用在每个资源表里都增加一个新的字段,我们可以为资源创建一个特殊的表来记录work。如果我们使用了这个方案,我们需在wokrer里面存储resource id,来记录这个ID那个资源的。这个方案修改量比较小,我们想知道谁操作某个特定的资源有点困难,但我们只需要访问一个表,就可以很简单的获取所有某个节点操作的所有资源。
接下来,我们需要移除race,就像我上篇文章介绍的那样。但是我们也需要额外的变化依赖于我们如何处理资源的互斥。
我并不想再赘述我们如何如何的需要改变目前的本地锁机制,这已经在上篇文章介绍过了。
在社区有一个分歧,一部分人认为应该使用DLM解决资源锁,另一部分人认为应该避免让云管理员去部署和配置更多的软件(比如,redis、zookeeper等)
我个人理解是使用DLM去实现AA是一个中间方案,直到我们完全的实现AA。因为我们使用Tooz可以很轻松的实现,这也是为什么我更偏向这个选择。我们可以先快速实现它,然后后面的版本再把锁从Manager和driver中移除。
附注一点,一些driver可以移除锁只要我们移除了API竞争,然后添加一些缺失的锁。
DLM这种方案,仅会影响AA的部署,而对初始的没有其他影响。
但是,通常思考另一种选择是好事情,这仅仅是我喜欢的而不是说是最好的,那么另外一种选择就是使用资源的状态。
为了探测节点有没有挂掉,也为了对那些还没有就位的节点(或者还得一段时间才能恢复的节点)进行清理,每一个集群节点都会进行report,分别占用DB不同的行。
我们可以用现在的host字段,上报时使用“cluster@backend@hostend”或者使用一个新的字段backend或者cluster里面包含cluster@backend段,然后host还是放在host字段不变。
其实,这一点倒不那么重要,这只是实现细节而已。在任何一个scheduler节点,都会有个周期任务检查数据库的内容,然后创建创建一个key为cluster@backend的字典,并存在value里,如果他是不同节点的信息以及是否我们对这个节点完成了清理。节点起来后,将会把cleanup_done设置为False。
如果cluster中的任何一个节点up,那么这个服务就up。
对于那些已经down的节点,我们将进行cleanup操作,就像我们在voluem node启动时做的那样,然后把cleanup_done字段设置为True。所以我们在下一次这个task启动时不会检查这个任务。
如果多个scheduler尝试同时对一个node进行清理,或者scheduler和之前的备节点现在active了,并且同时对资源进行修复。由于我们在数据库做的是使用compare and
swap的原子变更,跳过失败即可,确保只有一个完成资源的清理。
我们上报容量使用cluser@backend作为host即可,我们不需要作任何变动。
与上周的方案不同,组织数据出错更简单,这也是这个方案简单的一个主要原因。
我们不需要关心我们是否和后端失去连接,也不用关心我们与消息队列失去连接
如果我们与数据库失去连接我们需要停掉一切正在进行的操作,最简单的就是在心跳周期方案做这个事情。我们可以停掉backend的一切操作。
当我们使用DLM,我们应该检查连接,连接丢失要停掉所有操作,并且停止发送心跳,因为我们做不了任何事。
]]>目前OpenStack对外提供的北向接口是以REST接口提供的,也就是说通过HTTP(HTTPS)接口进行请求,进行虚拟机或者卷等相关的操作。OpenStack提供I层基本的能力,比如创建、查询、删除虚拟机或者卷等操作,以OpenStack作为平台,对上提供用户接口,对下操作下层Driver完成对设备的操作,其大致的架构基本如下所示:
整个OpenStack提供的接口也都是无状态的,对外接口也非常简单,例如获取卷的详情可以通过volumes的接口,最终可以得到卷的详情信息:
一般调用这些接口的上层主要是一些编排性或者提高用户体验的应用,达到“点一个按钮”完成对资源创建或者查询的应用。
问题的现象是,上层对资源进行操作、查询的时候非常慢,而实际在上层应用的服务器节点通过curl命令去调OpenStack接口的时候,发现整体时延均在秒级以上,甚至一个非常简单的卷查询指令也耗费了1秒以上的时间,而通过ping指令去测OpenStack响应时,保持在200ms左右,那么中间的时间耗在了哪?
好吧,我承认这篇文章写了一年了。。。不装逼了,直接说结论吧。当时遇到一个非常奇葩的问题,就是任何请求都耗时非常久,最后发现原来是https搞得鬼。
对于正常的http请求来说,请求本身耗时大致是一个RTT(TCP的三次握手),而对于https,中间增加了SSL握手的时间,大概算下来是3倍+的时延。
重点呢就是想记录一下这个指令:1
curl -w "TCP handshake: %{time_connect}, SSL handshake: %{time_appconnect}\n" -so /dev/null https://www.baidu.com
可以得到类似的结果,也告诉了用户大致请求中间的耗时花费在哪了,结果就像这样:
TCP handshake: 0.049, SSL handshake: 0.163
好了,就酱紫,底下参考链接是当时看的一些资料。
SSL handshake latency and HTTPS optimizations.
大型网站的 HTTPS 实践(2):HTTPS 对性能的影响
HTTPS 要比 HTTP 多用多少服务器资源?
HTTPS连接的前几毫秒发生了什么
图解SSL/TLS协议
SSL/TLS协议运行机制的概述
SSL延迟有多大?
HTTPS研究(2)—分解HTTPS连接建立过程
在维基百科中,是这么定义的
一致哈希是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n个关键字重新映射,其中K是关键字的数量, n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。
我们在上文中已经介绍了一致性Hash算法的基本优势,我们看到了该算法主要解决的问题是:当slot数发生变化时,能够尽量少的移动数据。那么,我们思考一下,普通的Hash算法是如何实现?又存在什么问题呢?
那么我们引出一个问题:
假设有1000w个数据项,100个存储节点,请设计一种算法合理地将他们存储在这些节点上。
看一看普通Hash算法的原理:
算法的核心计算如下
1 | for item in range(ITEMS): |
具体的完整实现请参考normal_hash.py,输出是这样的:
Ave: 100000
Max: 100695 (0.69%)
Min: 99073 (0.93%)
从上述结果可以发现,普通的Hash算法均匀地将这些数据项打散到了这些节点上,并且分布最少和最多的存储节点数据项数目小于1%。之所以分布均匀,主要是依赖Hash算法(实现使用的MD5算法)能够比较随机的分布。
然而,我们看看存在一个问题,由于该算法使用节点数取余的方法,强依赖node的数目,因此,当是node数发生变化的时候,item所对应的node发生剧烈变化,而发生变化的成本就是我们需要在node数发生变化的时候,数据需要迁移,这对存储产品来说显然是不能忍的,我们观察一下增加node后,数据项移动的情况:
1 | for item in range(ITEMS): |
详细实现代码在normal_hash_add.py输出是这样的:
Change: 9900989 (99.01%)
翻译一下就是,如果有100个item,当增加一个node,之前99%的数据都需要重新移动。
这显然是不能忍的,普通哈希算法的问题我们已经发现了,如何对其进行改进呢?没错,我们的一致性哈希算法闪亮登场。
我们上节介绍了普通Hash算法的劣势,即当node数发生变化(增加、移除)后,数据项会被重新“打散”,导致大部分数据项不能落到原来的节点上,从而导致大量数据需要迁移。
那么,一个亟待解决的问题就变成了:当node数发生变化时,如何保证尽量少引起迁移呢?即当增加或者删除节点时,对于大多数item,保证原来分配到的某个node,现在仍然应该分配到那个node,将数据迁移量的降到最低。
一致性Hash算法的原理是这样的:
1 | for n in range(NODES): |
我们依然对其进行了实现consist_hash_add.py,并且观察了数据迁移的结果:
Change: 58897 (0.59%)
虽然一致性Hash算法解决了节点变化导致的数据迁移问题,但是,我们回过头来再看看数据项分布的均匀性,进行了一致性Hash算法的实现consist_hash.py:
Ave: 100000
Max: 596413 (496.41%)
Min: 103 (99.90%)
这结果简直是简直了,确实非常结果差,分配的很不均匀。我们思考一下,一致性哈希算法分布不均匀的原因是什么?从最初的1000w个数据项经过一般的哈希算法的模拟来看,这些数据项“打散”后,是可以比较均匀分布的。但是引入一致性哈希算法后,为什么就不均匀呢?数据项本身的哈希值并未发生变化,变化的是判断数据项哈希应该落到哪个节点的算法变了。
因此,主要是因为这100个节点Hash后,在环上分布不均匀,导致了每个节点实际占据环上的区间大小不一造成的。
当我们将node进行哈希后,这些值并没有均匀地落在环上,因此,最终会导致,这些节点所管辖的范围并不均匀,最终导致了数据分布的不均匀。
详细实现请见virtual_consist_hash.py
1 | for n in range(NODES): |
输出结果是这样的:
Ave: 100000
Max: 116902 (16.90%)
Min: 9492 (90.51%)
因此,通过增加虚节点的方法,使得每个节点在环上所“管辖”更加均匀。这样就既保证了在节点变化时,尽可能小的影响数据分布的变化,而同时又保证了数据分布的均匀。也就是靠增加“节点数量”加强管辖区间的均匀。
同时,观察增加节点后数据变动情况,详细的代码请见virtual_consist_hash_add.py:
1 | for item in range(ITEMS): |
100000
101000
Change: 104871 (1.05%)
然而,虚节点这种靠数量取胜的策略增加了存储这些虚节点信息所需要的空间。在OpenStack的Swift组件中,使用了一种比较特殊的方法来解决分布不均的问题,改进了这些数据分布的算法,将环上的空间均匀的映射到一个线性空间,这样,就保证分布的均匀性。
代码实现见part_consist_hash.py
1 | for part in range(2 ** LOG_NODE): |
Ave: 100000
Max: 157298 (57.30%)
Min: 77405 (22.59%)
可以看到,数据分布是比较理想的。如果节点数刚好和分区数相等,理论上是可以均匀分布的。而观察下增加节点后的数据移动比例,代码实现见part_consist_hash_add.py
1 |
|
结果如下所示:
Change: 2190208 (21.90%)
可以看到,移动也是比较理想的。
深入云存储系统Swift核心组件:Ring实现原理剖析
Basic Hash Ring
Partition Ring vs. Hash Ring
Python中,打开文件的操作是非常常见的,也是非常方便的,那么如何优雅的打开一个文件?大部分的同学会这样实现:
1 | with open( "a.txt" ) as f : |
大家都知道,这样写可以自动处理资源的释放、处理异常等,化简了我们打开文件的操作,那么,with
到底做了什么呢?
从《Python学习手册》中是这么描述的:
简而言之,with/as语句的设计是作为常见try/finally用法模式的替代方案。就像try/finally语句,with/as语句也是用于定义必须执行的终止或“清理”行为,无论步骤中是否发生异常。不过,和try/finally不同的是,with语句支持更丰富的基于对象的协议,可以为代码块定义支持进入和离开动作。
也就是说对于代码:
1 | with expression [as varible]: |
with语句的实际工作方式:
1.计算表达式,所得到的对象是环境管理器,他必须有enter,exit两个方法。
2.环境管理器的enter方法会被调用。如果as存在,enter的返回值赋值给as后面的变量,否则,被丢弃。
3.代码块中嵌套的代码(with-block)会执行。
4.如果with代码块会引发异常,exit(type,value,traceback)方法就会被调用。这些也是由sys.exec_info返回相同的值。如果此方法返回为假,则异常会重新引发。否则,异常会中止。正常情况下异常是应该被重新引发,这样的话传递到with语句外。
5.如果with代码块没有引发异常,exit方法依然会调用,其type、value以及traceback参数会以None传递。
with/as语句的设计,是为了让必须在程序代码块周围发生的启动和终止活动一定会发生。和try/finally语句(无论异常是否发生其离开动作都会执行)类似,但是with/as有更丰富的对象协议,可以定义进入和离开的动作。
with/as语句的设计的初衷,在PEP343中是这么描述的:
This PEP adds a new statement “with” to the Python language to make it possible to factor out standard uses of try/finally statements.
In this PEP, context managers provide enter() and exit() methods that are invoked on entry to and exit from the body of the with statement.
对于下面的操作:
1 | with EXPR as VAR: |
等价于
1 | mgr = (EXPR) |
我们可以看到上述代码完整的处理了初始化及异常/正常场景的清理操作,这便是with
的设计思想,化简了冗余的代码,把那些重复的工作以及异常处理操作交给写“EXPR”源码(比如open操作)的同学。
我们继续深入的看下Python3中enter和exit的实现:
1 | class IOBase(metaclass=abc.ABCMeta): |
和我们预期的一致,在enter中返回了这个IO对象,然后在exit中,进行了清理。
先上一张大图,可以说这篇文章目的就是解释这个图:
存储系统从整体的分层一览,包括了主机/应用,存储介质,存储网络。对于存储来说就做了一件事:
Here is a bit of data. Hold onto it. Give that same bit back to me when I ask for it.
每个系统都有会有很多应用程序运行在CPU上,对于这些Application来说,他们觉得自己有很多很多足够的可用内存。
计算机系统中有一个“内存管理单元”(MMU,Memory Management Unit)的概念,MMU负责与DRAM内存直接通信,并且获得一些可用的“页”
多租户:内存被某个进程(比如一个应用)独享,这些内存不能被其他进程重写。
地址:将CPU的物理地址翻译成独有的DRAM地址或者是很多行DRAMs
有了MMU以后,对于每个进程来说,他们就像是一个人独占了所有的内存一样。
Application在他需要访问的时候,获取这些内存,在实际进行访问时,会发生如下事情:
在2.2中,获取的那些内存,实际是可以分布在内存中的任何地方的(非连续),MMU在把这些内存给应用的时候会进行初始化操作,当然,当应用不再访问这些内存页的时候,也会MMU也会负责回收这些内存。
如果Application对内存的访问,每次都要都要从MMU获取的话,那太慢了,因此有个经验法则是:
Always put storage/memory as close to the CPU as possible
可以说在存储中,改善时间的限制是永恒不变的主题,然后访问DRAM的话,需要60-100ns的时间。我们需要更快的访问,甚至达到“0”时间访问。
于是在CPU中增加了一些缓存,当然缓存也是分级别的,对于L1缓存大概花费1ns以内,L2缓存大概花费3-6ns,没错,这使得每次访问从60-100ns提升了几十倍!
如果DRAM中也没有足够的空间时,这是你需要更多的持久化存储,比如说磁盘。当然直接去访问磁盘会非常的昂贵,当然这里指的是时间的花费非常昂贵。
在Jeff Dean大神的Software Engineering Advice from Building Large-Scale Distributed Systems给出了数据:
L1缓存、L2缓存、主存、硬盘的访问分别是1ns、5ns、100ns、10,000,000ns级别;
举个比较形象的例子:你需要快递送个包裹,
快递一个1公里(L1)、5公里(L2)外的包裹,oops,可能马云爸爸保证当日达;
快递100公里(DRAM)的包裹,11点前下单,当日达,不能再快了!;
快递1000万公里(Disk)的快递,啊亲,你真的需要快递吗?这可是从地球到月球(38.4万公里)十几个来回的距离啊亲。
存储设备和RAM不太一样,不会和CPU直接进行“对话”,而是有一些其他的部分来帮助他们完成对话,我们在存储视角进一步去讲述。
块存储的IO流
CPU和内存通过PCI总线(目前通常是PCIe)与存储进行连接,应用会向存储请求一段数据。系统会将数据转换成地址和位置信息,并转换成某种协议的形式。
数据总线
CPU的指令需要转换或者说是进行适配,以便能够与存储设备进行“交流”,比如SCSI、IDE/ATA
SCSI系统
SCSI是一个很普遍、且向后兼容的,通常主要包含的组件有:
块设备
Block:块是存储介质上的物理或者逻辑单元,也是文件系统或者磁盘写入的最小单元,所有的存储(包括文件存储、对象存储)最终都是需要与block进行对话。
磁盘包括了盘面(Platter)、磁道(Track)、扇区(Sector)。盘面是一个圆,磁道是一个环,扇区是环上的一段,数据的位置影响性能。块由扇区组成,每一个块也有一个唯一的号码,文件系统的一切操作都是由对blocks操作构成。
对于应用来说,他们看到的都是文件;而对于存储来说,看到是块。因此需要某种方式将他们联系起来。对于文件来说,应用看到的是一个连续的“空间”,然而实际上,文件是由很多块组成的,而这些块就是磁盘上的块,这些块分布在磁盘的不同区域。
在操作系统中,其内核中会有一个文件系统,文件系统维护着在磁盘上的文件名,文件系统知道这些文件与磁盘上块位置的对应关系,同时还需要管理着磁盘空间、权限、所属、加密、文件缓存等等。
驱动控制:硬盘被驱动控制来操控,接收文件系统的通过某种协议(比如SCSI)下发的一些I/O命令
卷管理:文件系统和设备驱动中间存在着卷管理,卷管理负责抽象出一层“假的”磁盘供操作系统使用.
因此,对于文件系统来说,需要将应用程序所看到的“虚拟地址”翻译到真正的设备地址。例如访问一个文件时,文件系统会先会先在找到对应的逻辑块地址(Logical Block Adress, LBA),然后通过scsi系统进一步访问,对应到磁盘上的物理块地址。
Inode是文件的原数据,用来记录文件的所述、权限、block信息等。一个Inode会对应一个文件或目录,通过inode就可以将文件与某些block对应起来。
也就是说通过inode信息,就可以完全的找到一个文件,包括这个文件所对应的data block。
对于目录而言,data block包含了目录的内容,即该目录下文件及其这些文件对应inode的列表。
对于文件而言,data block则包含了文件的实际内容。
当执行cat /home/foo.txt
命令时,会对foo.txt文件进行访问,经历了以下步骤:
至此,我们可以看到存储数据是这样走自己的路的:
我们可以看到,访问/home/foo.txt有3次磁盘访问:
第1次是读取home的inode,获取foo.txt的inode号
第2次是读取foo.txt的inode,获取data block号
第3次是读取data block,获取真正的内容
然而,我们对于机械硬盘来说,每次需要磁盘转到正确的地址才能访问到内容,尤其是这些data block若未按顺序存储,就需要“下一圈”的时候再访问,这样会很耗时,也就是说访问磁盘的时间和数据在磁盘上的位置非常相关。所以Flash技术(如flash-based SSD)就腾空出世了,可以做到任意数据的随机访问,就大大减少了数据访问时间。
我们先回顾一下到网络这里都经历了什么:应用、操作系统、文件系统、缓冲缓存、卷管理、设备驱动、硬件控制、SCSI,一层一层递进访问。
应用和存储视角已经可以工作了,而在应用和存储之间加一层网络,因此目前的玩法主要包括以下几种形式:
这些知识在之前的存储基础知识学习中有大概的涉及过。
对于文件存储,常常使用NAS的架构,更灵活;而基于块的存储则常使用SAN的方式,保证性能。
下面是存储区域网络(SAN)常见的几种网络协议格式以及组网方式:
至此,我们已经将整个数据存储相关的部分都进行了解析,这下对存储数据包的一生有所了解了吧。
如图所示,主要流程分为两大部分:
参考资料:
http://blog.csdn.net/hackerain/article/details/7888686
http://www.openstack.cn/?p=437
http://lynnkong.iteye.com/blog/1829960
http://docs.openstack.org/developer/nova/services.html
http://www.cnblogs.com/sammyliu/p/4272611.html
http://www.cnblogs.com/littlebugfish/p/4022907.html
当Nova volume-attach server volume执行后,主要经过以下几步:
a. Nova Client解析指令,通过RESTFUL接口访问nova-api;
b. Nova API解析响应请求获取虚拟机的基本信息,然后向cinder-api发出请求保留,并向nova-compute发送RPC异步调用请求卷挂载;
c. Nova-compute向cinder-api初始化信息,并根据初始化连接调用Libvirt的接口完成挂卷流程;
d. 进而调用cinder-volume获取连接,获取了连接后,通过RESTFUL请求cinder-api进行数据库更新操作。
(1) \nova\nova\api\openstack\compute\contrib\volumes.py
在Nova Client进程中,由VolumeAttachmentController接受挂载请求
(1) \nova\nova\compute\api.py
VolumeAttachmentController的create函数用于响应卷挂载的请求。
(2) \nova\nova\volume\cinder.py
compute.API调用attach_volume函数,分别获取卷信息、检查状态并做保留盘操作
(3) \nova\nova\compute\rpcapi.py
通过attach_colume发送rpc调用Compute中的_attach_volume函数
(1) \nova\nova\compute\manager.py
ComputeManager进行核心调用,首先获取initiator,然后初始化连接。
(2) \nova\nova\virt\block_device.py
DriverVolumeBlockDevice初始化连接后调用connect_volume函数进行卷的挂载
(3) \nova\nova\virt\libvirt\volume.py
LibvirtISCSIVolumeDriver的connect_volume是调用最核心流程,分为多路径和单路径两种情况,在单路径的调用中会执行login、检查session、设置自启动等操作,如果一次未连接成功则还会每tries \ 2秒重复调用,直到达到调用的限制。其中牵扯到的指令有:
a. 尝试连接
iscsiadm -m node -T target_iqn -p target_protal
b. 连接失败重新建立连接
iscsiadm -m node -T target_iqn -p target_protal -op new
iscsiadm -m node -T target_iqn -p target_protal –op update -n node.session.auth.authmethod -v auth_method
iscsiadm -m node -T target_iqn -p target_protal –op update -n node.session.auth.username -v auth_username
iscsiadm -m node -T target_iqn -p target_protal –op update -n node.session.auth.password -v auth_password
c. 检查session,登陆
iscsiadm -m session检查是否登录成功
iscsiadm –m node –T targetname –p ip –login 登陆建立session
d. 设置为随机器启动而启动
iscsiadm -m node -T target_iqn -p target_protal –op update -n node.startup -v automatic
iscsiadm -m node -T target_iqn -p target_protal –rescan
(1) \cinder\cinder\volume\api.py
volume.API会继续调用VolumeAPI进行挂卷的数据库更新
(2) \cinder\cinder\volume\rpcapi.py
VolumeAPI通过rpc调用VolumeManager
\cinder\cinder\volume\manager.py
VolumeManager会完成更新数据库的操作。
在前面学习代码的过程中,主要通过源码来学习,开始学起来确实有点费劲,因为欠缺对OpenStack的整体的意识,于是搭建OpenStack开发环境对OpenStack的运行环境和使用有了初步认知。也看到了启动OpenStack后的一些相关进程,那么这些进程是如何与源码对应起来的呢?如何去调试OpenStack呢?本篇文章就讲下我的探索。
在Python 代码调试技巧一文中提到了pdb、PyCharm、PyDev、日志等几种常见的调试方法。具体可以看看这篇文章,写的很详细,不赘述。
因为PyCharm出色的用户体验(那写代码就是要开心嘛),所以决定使用PyCharm进行调试,但是问题来了,在远端(如虚拟机或者服务器)服务已经启动起来了,我们如何进行调试呢?答案就是Remote Debug。
在搭建OpenStack开发环境一文中,我们介绍了使用devstack启动开发环境,我们通过DevStack启动各个服务后:
1 | # ... ... |
使用screen来查看:
Note(2018.01.18): 目前社区已经废弃了screen这种方式来启动进程了(参考Remove screen support from devstack completely),原因是OpenStack需要管理的进程太多了,screen已没有“能力”去管理。
社区已经切换到systemd来进行进程管理,可以参考官方提供的Using Systemd in DevStack一文来入门。
1 | screen -x stack |
不得不说screen真是神器,虚拟了多个Terminal的Tab,使用”Ctrl+A+P”和”Ctrl+A+N”可以切换tab,使用”Ctrl+A+D”可以断开连接,在每个tab中可以使用“Ctrl+C”来中断进程:
我们看到在图中,有Nova和Cinder相关的进程,并且停在了cinder-api的进程上,每个tab中的进程都在运行着。
因为代码大部分都在远端的运行(比如虚拟机),而开发环境则在近端(比如宿主机),如果在远端和近端都维护一套代码,不可避免的会拷来拷去,有时拷错了还得找半天原因。所以得想办法把远端的代码“共享”到近端。因此,我们使用sshfs共享文件:
1 | ➜ ~ sshfs stack@192.168.56.101:/opt/stack /Users/jiangyikun/development/openstack/code |
达到的目的就是,我们编辑/Users/jiangyikun/development/openstack/code
的时候,就相当于在远端编辑/opt/stack
。在Windows下,也有win-sshfs工具。
当我们修改好代码后,就可以进行调试了。调试的原理大致是在近端启动一个debug server,然后,在代码中添加连接服务器的动作,这样,当代码运行到那段调试代码时,便会和调试服务器建立连接。在我的实验环境中,调试环境是这样的:
可以看到在宿主机和虚拟机有2条通路,一条是NAT,作用是让虚拟机通过宿主机的公网IP上网,从而保证Devstack能够顺利启动OpenStack,第二条是Host Only,保证在宿主机内可以对虚拟机进行SSH访问、sshfs文件挂载以及调试。
因此我们先配置一下远程调试的配置:
然后,我们就可以把由于几个调试的服务都启动起来了,例如,我们要调试跟踪Cinder的创建过程,我们就首先建立三个远程调试,其次将调试代码添加到入口处并保存,最后增加断点:
使用Ctrl+c把修改过代码的进程都结束,然后按“上”重新执行指令:
重启服务后,代码就生效了,当代码运行到我们需要连接到调试服务的代码后,就会进入断点了:
接下来就随心所欲的进行调试吧!
使用DEVSTACK搭建OPENSTACK可remote debug的开发测试环境
DevStack-install-in-China
删除卷流程比较简单,主要就是cinder-api解析Cilent的指令,并响应,发送RPC调用cinder-volume的delete操作,详细流程如下:
a. Client发送删除指令,通过RESTful接口访问cinder-api;
b. Cinder-api解析响应请求,通过RPC调用cinder-volume;
c. Cinder-volume通过调用Driver的delete函数进行删除。
(1) Cinder\api\v2\volumes.py
VolumeController的delete函数响应请求,首先从API获取Volume对象信息,然后,调用API的delete对对象进行删除;
(2) Cinder\volume\api.py
API.delete的对卷的状态进行检查,并更新状态为“deleting”,然后调用rpcapi的delete_volume函数
(1) Cinder\volume\rpcapi.py
VolumeAPI函数投递一个远程消息,通过消息队列远程调用cinder volume的delete_volume函数。
(2) Cinder\volume\manager
最终通过VolumeManager调用dirver的delete_volume对卷进行删除。
如整体架构图所示,创建卷涉及的答题步骤主要有以下几步:
a. Client发送请求,通过RESTFUL接口访问cinder-api。
b. Api解析响应请求,api解析由Client发送来的请求,并通过rpc进一步调用cinder-scheduler。
c. Scheduler对资源进行调度,scheduler选择合适的节点进行。
d. Volume调用Driver创建卷,volume通过指定Driver进行卷的创建。
代码的整体流程如下所示:
从上图可以看出,整体处理流程包括三大部分,分别是API、Scheduler、Volume三部分。
(1) cinder\api\v2\volumes.py
VolumeController. create函数对创建请求进行响应,首先函数对volume_type、metadata、snapshot等信息进行检查,然后调用Volume API的create进行创建。
(2) cinder\volume\api.py
API.create函数对source_volume、volume_type等参数进行进一步检查,并调用cinder.volume.flows.api.get_flow来创建。
(3) cinder\volume\flows\api\create_volume.py
get_flow函数检查Quata,最后创建EntryCreateTask及VolumeCastTask等任务,
其中EntryCreateTask会将卷的创建过程写入数据库,此时卷的状态为”creating”。
VolumeCastTask.excute函数会调用VoumeCastTask._cast_create_volume
VolumeCastTask._cast_create_volume函数,如果未传入host,则会经过调度进行创建卷,通过scheduler_rpcapi.create_volume创建卷;如果未传入host则直接交由Volume Manager去创建卷。
至此为止,Cinder API部分完成了自己的工作。
(1) cinder\scheduler\rpcapi.py(此步还属于cinder-api)
SchedulerAPI.create_volume函数会通过消息异步调用SchedulerManager.create_volume函数。
(2) cinder\scheduler\manager.py
SchedulerManager.create_volume函数,使用自己的flow来创建volume,其中还传入了Driver。
(3) cinder\scheduler\flows\create_volume.py
get_flow函数,创建ScheduleCreateVolumeTask
ScheduleCreateVolumeTask.execute函数,会调用driver_api.schedule_create_volume
(4) cinder\scheduler\filter_scheduler.py
FilterScheduler. schedule_create_volume函数,更新数据库,最后通过消息队列请求调用volume_rpcapi.create_volume。
(1) /cinder/volume/rpcapi.py(此步还属于cinder-scheduler)
VolumeAPI.create_volume会通过消息队列远程调用VolumeManager.create_volume
(2) /cinder/volume/manager.py
VolumeManager函数也使用flow来创建volume,执行CreateVolumeFromSpecTask这个任务
(3) /cinder/volume/flows/manager/create_volume.py
CreateVolumeFromSpecTask.excute,这个函数会根据创建的不同类别,去创建卷,例如调用create_raw_volume,最终会调用具体的driver进行卷的创建。
在完成创卷后,CreateVolumeOnFinishTask这个任务,启动更新数据库,将卷更新为available状态。
我们可以看到在创建卷的过程中盘的状态会从“creating”状态变为“available”状态。
]]>整体的环境安装是基于[devstack]来搭建,本来就是一个脚本一个配置文件就可以了,不过因为部分网络环境比较“艰苦”,所以需要做一些优化,所以需要做一些准备工作。
Virtual Box,下载
Ubuntu 14.04,下载
网络的话,我是选择了NAT模式,保证虚拟机能够通过宿主机来上网,然后另设了一个Host Only网卡做SSH访问,最近突然发现还可以用NAT端口转发的方式(把22端口转发到主机)完成类似功能,这样就只需要一个网卡了。
1 | 安装Git |
为了加速下载速度,对Python源进行优化,这里用豆瓣的源。
1 | # vim ~/.pip/pip.conf |
为了加速下载速度,对Ubuntu源进行优化,这里用网易的源。
1 | # vim /etc/apt/sources.list |
使用Git安装脚本切换到自己需要的版本:
1 | # 下载devstack |
1 | devstack/tools/create-stack-user.sh; su stack |
创建local.conf文件,并且写入自己的配置,可以参考官方的Minimal configuration,我的配置如下所示,参考链接做的。
1 | # vim ~/devstack/local.conf |
1 | cd devstack; ./stack.sh |
最终安装完毕:
1 | #... ... |
问题: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately [duplicate] 参考链接解决的:
1 | sudo apt-get install libffi-dev libssl-dev |
问题: ERROR: tox version is 2.1.1, required is at least 2.3.1
1 | sudo pip install 'tox==2.3.1' |
Ubuntu 14.04 Devstack安装Liberty
使用DEVSTACK搭建OPENSTACK可remote debug的开发测试环境
DevStack - an OpenStack Community Production
stack.sh
Minimal Configuration
磁盘大致由盘片、磁头、步进电机等几部分组成组成。
盘面:硬盘一般含有一个或多个盘片,一个盘片包含两个盘面。
磁道:每个盘面被划成多个狭窄的同心圆环,这样的圆环叫做磁道。
扇区:每个磁道的每段圆弧叫做一个扇区,是读写的最小单位。
柱面:所有盘面上的同一磁道,在竖直方向构成一个圆柱,称为柱面。
读写过程:硬盘读取数据时,磁头先移动到读取扇区所在磁道的上方,这个过程耗时叫做磁盘寻道时间,平均时间为10ms。之后,通过盘片的旋转,使得扇区转到磁头的下方,这个过程耗时叫做旋转延迟时间,对于7200转/min的硬盘转一周为60*1000/7200=8.33ms,平均旋转延迟为4.17ms(半圈)。
RAID(Redundant Array of Independent Disks),即由独立的磁盘组成的具有冗余特性的阵列。其基本思想就是把多个相对便宜的硬盘组合起来,成为一个硬盘阵列组,使性能达到甚至超过一个价格昂贵、 容量巨大的硬盘。
RAID 0,条带化存储,容量增加,并行化,但无冗余,容易单点故障。
RAID 1,镜像存储,写入速率慢,读取速率快,有冗余备份,优点是高可靠、高可用,缺点是高花费。
RAID 2,RAID 0的改进版,使用汉明码进行检测和纠错,适用于连续IO、大块IO(如视频流)。
RAID 3,RAID 3和RAID 2的思路比较相似,使用奇偶校验进行错误检测和纠错,但校验盘单点故障。
RAID 4,RAID 4和RAID 3思路一样,只不过是使用BLOCK进行存储。
RAID 5,校验信息交叉的存储在所有数据盘上,高冗余,高数据传输率,实现复杂。
RAID 6,相比RAID5增加块内的校验,允许同时坏2块硬盘而不丢失数据。
RAID 01,先做条带(0),再做镜像(1)。读写速度快,数据保护能力强,空间利用率50%。
RAID 10,先做镜像(1),再做条带(0)。
根据网上的资料和理解,用Visio整理了一张图对比了下几种方式:
DAS全称为Direct Attached Storage,即服务器直连存储。如图所示,文件系统直接通过RAID完成对硬件访问。优点是操作简便,经济,缺点是分散式存储,不可集中管理。
NAS全称为Network Attached Storage,即网络存储服务。如图所示,文件系统通过网络暴露出来给应用服务。优点是结构简单。配置使用管理非常方便,可实现跨平台的数据共享。缺点是需要占用网络资源、应用局限性大。
SAN全称为Storage Aera Network,即存储区域网络,如图所示,RAID接口通过网络暴露出来。优点是扩展性强,集中管理,缺点是成本较高,管理维护难度大。
FC SAN指基于光纤通道(Fiber Channel)的存储区域网,在FC SAN中存在两张网,一张面向应用的网(IP网),另一张中则是存储网(FC网)。而IP SAN的出现则是为了寻求一种新的方式,用与应用网相同的体系架构来构造存储网,使用通用的IP网络及设备。
FC SAN性能好,价格高,但与主流的IP网络异构。适用于关键应用的几种存储、备份及容灾。
IP SAN则由于以太网MTU(1518字节)的限制,性能稍差,但基于通用的IP协议。适用于异地间的数据交换、备份容灾,非关键应用的集中存储。
LVM的全称是Logical Volume Manager,逻辑卷轴管理,主要解决的问题是,弹性调整文件系统的容量。
与传统的磁盘与分区相比,LVM为计算机提供了更高层次的存储,通过在磁盘分区和文件系统之间增加一个逻辑层,提供一个抽象的逻辑盘卷。
Nova uses the oslo.i18n library to support internationalization. The oslo.i18n library is built on top of gettext and provides functions that are used to enable user-facing strings such as log messages to appear in the appropriate language in different locales.
Nova exposes the oslo.i18n library support via the nova/i18n.py integration module. This module provides the functions needed to wrap translatable strings. It provides the _() wrapper for general user-facing messages and specific wrappers for messages used only for logging. DEBUG level messages do not need translation but CRITICAL, ERROR, WARNING and INFO messages should be wrapped with _LC(), _LE(), _LW() or _LI() respectively.
理解:Nova是通过oslo.i18n来支持国际化的,oslo.i18n是基于getnext做的,这个库可以把面向用户的字符(比如日志)翻译成指定的语言。其中DEBUG信息不翻译,其他的信息会被翻译。
比如:
1 | # debug log |
Do not use locals() for formatting messages because: 1. It is not as clear as using explicit dicts. 2. It could produce hidden errors during refactoring. 3. Changing the name of a variable causes a change in the message. 4. It creates a lot of otherwise unused variables.
If you do not follow the project conventions, your code may cause hacking checks to fail.
另外,文中提到了不要使用locals()
去格式化消息主要4点原因:1.不清楚是否有关键字. 2.重构时会有潜在的出错可能. 3.变量名变了消息就变. 4.创建很多无用的变量。
这些函数_(), _LC(), _LE(), _LW() and _LI()可以通过以下方法导入:
1 | from nova.i18n import _ |
年初的计划是百度,腾讯,阿里的任何一个地方实习。当时心中最想去的是腾讯或者阿里,对百度不感兴趣的原因主要是在北京呆了太久了,新鲜感没了:)。
对于实习整体对自己比较满意,算是把一个目标分解到执行实施的一个完整过程。从面试准备到拿到offer,从开始实习到实习转正,都有条不紊按照自己的计划一步一步地实施。有预判,有变化,提前准备,未雨绸缪,占据主动,值得再接再厉。
实习面试准备得还算充分,从过年那会儿就开始准备了,补Java的基础,看Collection的源码,刷leetcode。在三月,实习的提前批就拿到了蚂蚁金服支付宝研发的offer。
实习的过程中,也看到了一个互联网企业中人们的生活状态:苦逼而快乐。加班是常态,不过抱怨的人不多,大概是大家都有一颗为了自己或者阿里付出的心吧。组内的氛围很好,没有主管,组员什么的分别。总得来说,真的是开开心心地上班,每个人都有存在感,每个人都有机会,牛人有更大的舞台。和主管,师兄,师傅学到了很多做人处事的方法或者态度,谦逊努力,积极思考,善于沟通。
实习结束后的转正答辩也比较顺利,当时是拿到了HR的口头offer,不过预估自己可能不会留下,在走的时候,也没给主管一个确定的回复,感觉挺抱歉的,找完工作后也和师兄,师傅他们聊了一些,也得到了很多的发展和工作的建议,也算是解了自己的心结。
花名不改,江湖犹在,后悔有期,就像一弦哥说的那样:世界很小的,有心的话很快就会再见面。
今年新添了个设备,13寸的Macbook Pro,惯例的,还是花自己的钱。
首先是自己对mac的印象比较好,促使我买mac主要有几点吧,一是大家都说mac是程序员的神器,买的人都说谁用谁知道;二是大致预料到可能今年不买以后会更犹豫。最后一点是,真的有买mac的钱了,是的,没错这才是最重要的一点。
于是,在北京的时候,用学生价在西单大悦城的实体店买的,从决定买到买到手大概就是一个早上。总体来说,比较满意,和预想的差不多,最开始的时候只有对command键不太适应,其他基本顺畅,基本平滑过度。现在的日用,技术博客,编码基本都适应了。最近配合着新买不久的数位板,也用漫画的形式了记录了一下研究生的生活,目前线稿基本搞定,该上色了。:)
总觉得年轻时应该有一些“意气用事”的决定,因为我们年轻,有承担失败的试错资本,也可以尽情地享受意外的惊喜,想到什么就去做吧,开心就好。
其实对于找工作来说,和其他大多数同学来说,不算太艰辛, 算下来面试过腾讯,百度,阿里,头条,华为,小米,IBM一共7家。
在实习的时候,有个周末去深圳参加了腾讯提前批的TST面试,顺利拿到offer,好的开始也奠定了后期找工作的心态,后面阿里这边也顺利转正拿到口头offer,因为觉得留在杭州的可能不大,所以走的时候就没和部门说定,发生了拥抱变化的故事后,也便不了了之。在随后的面试中,也拿到了北京小米的sp offer,比较意外的是居然还有北京户口,然后就是华为的sp offer。其中,百度和头条挂掉的原因总结一下也是因为算法面试准备的不充分,想想没有集齐BAT召唤神龙,有点可惜。然后IBM面试都搞定了,流程比较慢,有缘再会。最终,综合考虑各种因素,选择了西安华为作为工作开始的地方。
单从工作性质来讲,华为并不是最优选择, 其实拿到offer的这些在我心中都是很好的选择,都有很多诱惑的地方,留西安确实是一个很重要的因素,对于这个选择,原因只有三个字:不辜负。现在描述这些的时候真的很平静,也许是因为很早就想通的原因,也一定会既来之,则安之,化那些不甘为努力奋斗。
每个阶段都有自己的选择,也希望以后再做选择时,能够想清楚自己真正想要的是什么,选择自己最珍视的东西,不被诱惑,不被干扰,相信自己的感觉,自己满意就好。
计划是每月大概一本的样子,为了记录一下读书的过程,写了个插件把豆瓣的读书记录同步到自己的博客了。
今年读了7本书,不过每本都是认认真真的读。今天还和室友聊起了读书数目的问题,大致达成的结论是:读书要有质量,要有感受,不要太盲目。
另外,自己的技术博客也慢慢积累起来了,期望在未来能够学有所思,不断积累。
2015,这一年
前面依着自己的计划,算是把2015这一年基本覆盖了,2015这一年待过不少城市,在北京科研,去芭提雅玩,去曼谷参加姐姐的婚礼,在杭州阿里实习,去上海找兄弟聚,去深圳腾讯面试,去三亚Outing,到成都和银川毕业旅行,总得来说是在忙碌中四处穿行。
2015对自己比较满意的是能够按照自己的思考一步一步地把一些事情做好,而这些事情主要归功于提前的计划和不断的思考,不过还是有些地方没有思考全面。2015对自己不太满意的是在一些有压力的时候会有些不知所措,遇到未知的事情不够淡定,犹豫不决,决策能力需要提升。 以后肯定会越来越忙的,希望自己能够不断思考总结,迎接变化,更重要的是在任何时候都不要迷失了自己。
未来一年对自己的期望是:技术更加专注有深度,做人更加靠谱有内涵,生活更加丰富有惊喜。
最后,2015,再见,2016,你好!
]]>我们先看看对整型变量i进行赋值,并对i进行显示的过程:
1 | 1 i= |
在之前已经了解到对象是如何存储、组织的了,那么,整型是如何存储的呢?在Python 2中,整型是分Int和Long的,稍小一点的数直接用C语言中的long去存储,稍大一点的数(超过long的承受范围):
1 | Python 2.7.10 (default, Jul 14 2015, 19:46:27) |
而在Python 3中,我们进行相同的操作:
1 | Python 3.5.0 (default, Dec 20 2015, 21:24:49) |
我们发现不论这个数是“大”是“小”,都无差别的显示为“int”了。那么,在Python3中做了哪些变动呢?Python3如何“一统天下”大小,存储整型数据呢?在Python 2中,分别使用intobject和longobject去存储整型,而在Python 3中,则使用longobject统一的表示整型,并且将type也设为“int”,在PEP 237 – Unifying Long Integers and Integers中,详细的阐述了这个改变,下面让我们详细看看Python 3中整型的实现。
我们先看看longobject.h:
1 | typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ |
按着注释解释,发现了整型的庐山真面目在longintrepr.h中:
1 | /* Long integer representation. |
注释非常清楚,我们得到以下信息:
SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
。obsize==0
来表示。ob_digit[abs(ob_size)-1]
非0。i
,ob_digit[i]
的范围在[0, MASK]
。可以看到整型是借助柔性数组来实现的,柔性数组较指针来说,分配内存和释放内存时更为方便,且数组名本身不占用空间(指针需要占用1个int型的控件),柔性数组相关内容可以参考链接。
光看描述可能有点抽象,我们看一个具体的例子,在我的环境中,SHIFT为30,MASK则为2的30次方-1,1152921506754330628
这个数字在代码中是这样存储的,ob_size为3,ob_digit为4-2-1,我们来看看具体的表示含义:
很熟悉吧,其实就是个进制转换过程:2的30次方进制转换为10进制!没错,就是这么简单。
我们再观察一个现象:
1 | 10 i= |
我们发现对于第一个来说,i和j是一个对象,而第二个例子i和j是不同的对象。
可以想象的,如果每个整型都需要使用对象来表示,那么每创建一个整型就需要创建一个对象,这显然是不合理的,因此设计者将[-5, 257)范围内的整型数据进行了特殊处理,即在初始化的时候就将这些对象产生好,以后需要这些对象时直接从对象池取就好。
1 |
|
为了验证我们对整型实现理解的正确性,我们修改long_to_decimal_string_internal
中加入一些代码,打印出整型变量的详情:
1 | static int |
修改后,我们看到结果为:
1 | 2**60+2*2**30+4 |
我们看到,结果与我们预期的一样,ob_size为3,代表ob_digit的数组大小为3,ob_digit表述的数值为(2^60) x 1
+ (2^30) x 2
+ (2^0) x 4
。完整代码工程请见链接
PEP 237 – Unifying Long Integers and Integers
How does Python manage int and long?
How does python represent such large integers?
Python:How does C implements the Python assignment of large numbers
HACKING PYLONGOBJECT ON PYTHON 3.2
py3源码-2-整数
Python 整数对象实现原理
结构体中最后一个成员为[0]或[1]长度数组(柔性数组成员)的用法
我们在《Python3源码学习-对象》中提到了每个对象都含有一个type的属性,我们看看type是个什么东西,目光移到object.h:
1 | typedef struct _typeobject { |
可以看到,PyTypeObject就是类型对象的定义了。其中,包含了一些和类型相关的重要信息:
const char *tp_name
,例如对于整型对象,他的这个name就是“int”。tp_basicsize
和tp_itemsize
包含了创建该类型的对象所需要分配的控件大小。tp_dealloc
用于销毁对象,tp_hash
用于计算hash值,tp_str
用于将对象转换为str。另外可以看到,由该类的属性PyObject_VAR_HEAD
看出,类型也是一个对象。我们知道每个对象都会有一个类型,那么思考一个问题,类型对象的类型又是什么呢?这个问题在typeobject.c的文件中给出了答案:
1 | PyTypeObject PyType_Type = { |
可以看到类型对象的类型就是PyType_Type
,而PyType_Type的类型则执行它本身。
如上图所示,是一个简单的整型对象的例子,我们可以看出:
我们也可以看出因为“万物皆对象”,不同的object虽然type不同,但是都有一个类型指针,通过类型指针ob_type即可完成对应方法的访问,Python也正是利用了指针的特性,从而实现了多态。
]]>在官网下载最新源码,为了方便跟踪源码的修改,借助git来管理Python源码。
1 | git init |
在编译前需要使用configure对源码进行配置。
1 | ./configure |
配置完源码后,使用git status观察一下变化:
1 | ➜ Python-3.5.0 git:(master) ✗ git status |
我们看到产生了一些和make相关的配置文件,用来适配当前的环境。
用git提交一下:
1 | git add . |
编译Python时,使用make进行编译
1 | make |
编译完成后,产生了一个python.exe文件,便是我们编译所得的执行文件。
注
我是在OSX下进行编译的,在编译过程中遇到了fatal error: 'lzma.h' file not found
问题,把lzma包安上就可以了:
1
2 > brew install xz
>
1 | ./python.exe |
以上便完成了整个Python源码的编译。以后,在修改完代码以后,只需要make;./python.exe
就好~那么,享受Hack python code的乐趣吧。:)
代码已上传,并根据学习过程不断更新:https://github.com/Yikun/Python3
]]>在Python中,万物皆对象,那么对象又是什么结构,如何组织,怎样实现的呢?
Objects are structures allocated on the heap. Special rules apply to the use of objects to ensure they are properly garbage-collected. Objects are never allocated statically or on the stack; they must be accessed through special macros and functions only.(Type objects are exceptions to the first rule; the standard types are represented by statically initialized type objects, although work on type/class unification for Python 2.2 made it possible to have heap-allocated type objects too).
从Python的源码注释可以得到以下信息点:
然后,还补充说,Type对象除外,标准的type对象是静态初始化的,Python 2.2把在堆上初始化type对象变成了现实。
Python中的对象,主要分为一般对象和变长对象(list、dict之类),一般的对象就是PyObject,然后变长对象其实就是给PyObject加了个size成为了PyVarObject。
对于所有对象来说,均有2个重要的元素:
对于可变长的对象(比如list,dict),会多一个域:
另外头部还有_PyObject_HEAD_EXTRA
,这个宏定义了next和prev指针,用来支持用一个双链表把所有堆中的对象串起来。
1 | /* Define pointers to support a doubly-linked list of all live heap objects. */ |
Python-3.5.0源码
PYTHON 源码阅读 - 对象
Python源码剖析
二层交换指的是传统的二层交换机实现的功能,主要的功能就是将以太网帧从一个端口接收,并从合适的端口发送出去。
以太网帧如上图所示,如此构造主要是为了表达这个帧:到哪去(目的MAC,6字节)、从哪来(源MAC,6字节)、什么帧(长度/类型,2字节)、有什么(数据,40~1500字节)、错没错(FCS,CRC,4字节)。
用户数据的长度不同,以太网帧的长度也不同,范围为64-1518字节。
存储转发:转发时,常用的方式为存储转发方式(store forward),即数据帧先入存储队列再根据转发表进行转发。使用存储转发,一来可以为多种速率端口的数据进行缓冲,二来也可以将残损、CRC错误等异常帧进行丢弃。小科普:除了存储转发外,还有直通转发(cut through,只读到DA后就转发,无法处理冲突帧、CRC错帧)、碎片隔离(fragment free,读一个slot共64字节,无冲突再转发,无法处理CRC帧)。
地址学习:在完成存储转发时,需要查询转发表,从而得知数据帧应该从哪个端口发出,因此,转发表存的就是目的MAC地址与端口的映射,转发表生成的过程就是地址学习。大致过程就是,来一个帧,读他的源MAC,然后把源MAC和接收的端口号存下来。这样,交换机就知道了,从X端口来过MAC地址为Y的数据,当下次Y需要转发时,就可以把他转发到X端口了。从而,完成“从哪来去回哪去”的任务
在几个交换机构成环时,会产生广播风暴,造成广播风暴的根本原因是交换机之间不能感知到互相的存在,导致地址学习时,一个交换将某个MAC的转发端口学习成了另一个交换机的端口。可以通过STP协议进行抑制,从逻辑上“断开”环。
VLAN可以将广播域分隔为多个逻辑网段。从帧格式上来看,增加了VLAN相关的域,VLAN相关域包括0x8100标志位,然后3bit的优先级,1bit的丢帧优先级,12bit的VLANID。在做转发学习时,通过SA+VLAN来学习转发端口。值得注意的是:当某个DA+VLAN查不到表时,仅在VLAN域广播。
换一种思想来看,可以认为VLAN是一种网络的虚拟化,将一个端口虚拟化成多个端口。
路由的主要功能是根据目的IP转发到相应的网络中。和二层中的转发表类似,三层路由也存在一个类似的表,叫做路由表。
也就是说如果一个LAN希望连接另一个LAN,那么需要借助路由完成。另外,在大型的LAN中,由于连接设备多,导致MAC多,导致广播负担大,因此切分子网来避免这一问题,而子网之间所属不同LAN所以也需要借助路由完成通信。
在进行路由的时候,路由首先根据最长匹配原则在路由表中查找下一跳IP地址,之后,根据ARP表,获取下一跳的MAC信息,便进入ARP流程,最后,根据下一跳MAC地址生成以太网数据帧,并将该数据帧从接口转发至网络。
路由与三层交换有类似的地方,L3交换虽具有路由功能,但其核心功能主要在于数据交换上,而路由仅具有路由转发功能。
Network Adress Translator,网络地址转换。
对于源地址NAT,主要用于内网访问外网,源地址进行转换;
对于目的地址NAT,一般用于外网访问内网,目的地址进行转换。
由于目前大部分云计算服务器、网络设备都是运行在Linux上的,因此,学习一些和Linux网络底层相关的实现,有助于我们理解。如Linux 上的基础网络设备详解一文中所述:
Linux 用户想要使用网络功能,不能通过直接操作硬件完成,而需要直接或间接的操作一个 Linux 为我们抽象出来的设备,既通用的 Linux 网络设备来完成。一个常见的情况是,系统里装有一个硬件网卡,Linux 会在系统里为其生成一个网络设备实例,如 eth0,用户需要对 eth0 发出命令以配置或使用它了。
另外,对于Linux网络中的数据流在kernel flow中有所描述,文中更有一张神图。
之前,有做过网络驱动,其实回想起来,核心的实现就2个:发送函数和接收函数。
1. 发送函数(回调)
核心功能是将上层网络传来的帧,写入到网卡中;
在网络驱动初始化时,会通过注册的方式对网络驱动进行初始化,
1 | static const struct net_device_ops netdev_ops = { |
当上层有帧传来时,就会回调driver_xmit函数,因此,在driver_xmit函数中,就应当实现将帧写入到硬件,一般硬件会提供插入帧的接口,完成插入时帧会进入网卡硬件的插入队列中。
2. 接收函数(中断)
核心功能是将网卡中的帧,传送到上层协议栈中。
对于接收函数,则需要依靠硬件的中断,数据帧到达网卡,硬件以中断的方式告知系统,然后,接收函数回调,回调时,需通过硬件的接口读取数据帧,然后将其上传值上层接口。一般调用netif_rx()
进行传输。
Linux Bridge名释其意,像一个桥梁一样把网络设备桥接起来。Linux bridge是802.1D的实现,可以参考链接。
如上图所示是eth0和eth1加入到br0后的实现,可以看出,网桥向上屏蔽了桥下的网卡设备,从上层协议上来看,仅能看到网桥设备br0。
在《Understanding Linux Network Internals》一书中,分析了Linux Bridge的实现:
在br中有个链表来链net_device,每个net_device也反向链着br。在br中有个hash结构叫做fdb_entry,存储着转发表(forward databse),若某个MAC地址在fdb中,那么就直接发到某个net_device,如果没在的话,就广播给链接到该br的所有设备。
交换与隔离是VLAN的两大功能,现实世界中的802.1q交换机存在多个VLAN,每个VLAN拥有多个端口,同一VLAN的端口可数据交换,不同VLAN的端口之间隔离。而Linux VLAN实现的是隔离,需要交换的话,需要在Linux Bridge上attach一个VLAN。即Linux Bridge加VLAN device能在功能层面完整模拟现实世界里的802.1.q交换机。
在关于linux 802.1d (bridge) 和 802.1q(vlan) 实现的再思考一文中,举了个例子,觉得很不错,画了个图加深理解:
一个盒子有6个物理interface, eth0,eth1,eth2,eth3,eth4,eth5,eth6.
bridge0 { eth0, eth1, eth2 }, vlan id 是2
bridge1 { eth3, eth4, eth5 }, vlan id 是3
eth0,eth1,eth2,eth3,eth4,eth5都在混杂模式,并且没有ip地址,它们是bridge的port.
创建vlan interface, bridge0.2, bridge1.3。在bridge0.2和bridge1.3上配置ip地址。vlan 2的机器,把bridge0.2的地址设置为缺省网关;vlan 3的机器,把bridge1.3设置为缺省网关。当有包要从vlan 2发往vlan 3是,它将送到bridge0.2,然后,通过路由,找到bridge1.3,然后由bridge1.3发出去。这个过程中,packet里面的vlan id会发生改变。这个例子里面,要求从bridge port上收到的包都必须是打tag的,在bridge里面,并不能识别和处理tag,只有到三层的vlan interface才能识别并处理这些tag.
另外,Linux VLAN则是802.1Q的实现,可以参考链接
《云计算网络珠玑》
《 图解网络硬件》
kernel flow
linux bridge
《Understanding Linux Network Internals》
关于linux 802.1d (bridge) 和 802.1q(vlan) 实现的再思考
802.1Q VLAN implementation for Linux
All OpenStack services use green thread model of threading, implemented through using the Python eventlet and greenlet libraries.
Green threads use a cooperative model of threading: thread context switches can only occur when specific eventlet or greenlet library calls are made (e.g., sleep, certain I/O calls). From the operating system’s point of view, each OpenStack service runs in a single thread.
The use of green threads reduces the likelihood of race conditions, but does not completely eliminate them. In some cases, you may need to use the @lockutils.synchronized(…) decorator to avoid races.
In addition, since there is only one operating system thread, a call that blocks that main thread will block the entire process.
理解:OpenStack的所有服务都使用Green thread,使用eventlet和greenlet库,绿色线程使用协作并发模型,线程的切换只在eventlet或greenlet库调用一些切换时发生。从操作系统角度上来看,每个OpenStack运行在一个单一线程。用Green Thread的好处是能够减少race conditions,当然有些时候我们也必须使用@lockutils.synchronized(…)来完全避免。因为只用一个系统级别的单线程,所以调用一旦阻塞就会阻塞整个进程。
关于Python中的并发模型,可以参考Python并发模型一文,把Thread(线程切换耗资源)、MicroThread(依靠解释器调度)、Green thread(协作并发)的特点对比了下。
还有Python几种并发实现方案的性能比较将Python中的集中并发方案进行了对比和说明。
If a code path takes a long time to execute and does not contain any methods that trigger an eventlet context switch, the long-running thread will block any pending threads.
This scenario can be avoided by adding calls to the eventlet sleep method in the long-running code path. The sleep call will trigger a context switch if there are pending threads, and using an argument of 0 will avoid introducing delays in the case that there is only a single green thread:
1 | from eventlet import greenthread |
理解:对于那些耗时很长的任务,需要我们添加一些yield方法,来避免在单个的调用中阻塞很久。
Queries to the MySQL database will block the main thread of a service. This is because OpenStack services use an external C library for accessing the MySQL database. Since eventlet cannot use monkey-patching to intercept blocking calls in a C library, the resulting database query blocks the thread.
The Diablo release contained a thread-pooling implementation that did not block, but this implementation resulted in a bug and was removed.
理解:对于MySQL数据的查询会阻塞服务,因为eventlet对C库的调用是无法去做monkey-patching的。
]]>Host aggregates can be regarded as a mechanism to further partition an availability zone; while availability zones are visible to users, host aggregates are only visible to administrators. Host aggregates started out as a way to use Xen hypervisor resource pools, but has been generalized to provide a mechanism to allow administrators to assign key-value pairs to groups of machines. Each node can have multiple aggregates, each aggregate can have multiple key-value pairs, and the same key-value pair can be assigned to multiple aggregate. This information can be used in the scheduler to enable advanced scheduling, to set up xen hypervisor resources pools or to define logical groups for migration.
理解:Host Aggregates可以视为是AZ(Avaliability Zone)的更进一步的划分,是对管理员可见的。每一个节点都可以属于多个Aggregates,这些Aggregates可以用作更高级的调度、配置Xen的资源池、或者定义用于升级的逻辑分组。可以理解为aggregate是一组具有相同属性主机的分组。
Availability Zones are the end-user visible logical abstraction for partitioning a cloud without knowing the physical infrastructure. That abstraction doesn’t come up in Nova with an actual database model since the availability zone is actually a specific metadata information attached to an aggregate. Adding that specific metadata to an aggregate makes the aggregate visible from an end-user perspective and consequently allows to schedule upon a specific set of hosts (the ones belonging to the aggregate).
That said, there are a few rules to know that diverge from an API perspective between aggregates and availability zones:
- one host can be in multiple aggregates, but it can only be in one availability zone
- by default a host is part of a default availability zone even if it doesn’t belong to an aggregate (the configuration option is named default_availability_zone)
理解:Availability Zones可以理解为将一个aggregate加了一些metadata信息,使得对用户可见。他和Aggregate最主要的区别是,一个节点只能属于一个AZ,默认一个主机属于一个默认的AZ。
Originally all aggregates were Xen resource pools, now an aggregate can be set up as a resource pool by giving the aggregate the correct key-value pair.
You can use aggregates for XenServer resource pools when you have multiple compute nodes installed (only XenServer/XCP via xenapi driver is currently supported), and you want to leverage the capabilities of the underlying hypervisor resource pools. For example, you want to enable VM live migration (i.e. VM migration within the pool) or enable host maintenance with zero-downtime for guest instances. Please, note that VM migration across pools (i.e. storage migration) is not yet supported in XenServer/XCP, but will be added when available. Bear in mind that the two migration techniques are not mutually exclusive and can be used in combination for a higher level of flexibility in your cloud management.
理解:可以将Xen的划分到一个Aggregate,来支持Xen资源池的一些特性。
The OSAPI Admin API is extended to support the following operations:
- Aggregates
list aggregates: returns a list of all the host-aggregates (optionally filtered by availability zone)
create aggregate: creates an aggregate, takes a friendly name, etc. returns an id
show aggregate: shows the details of an aggregate (id, name, availability_zone, hosts and metadata)
update aggregate: updates the name and availability zone of an aggregate
set metadata: sets the metadata on an aggregate to the values supplied
delete aggregate: deletes an aggregate, it fails if the aggregate is not empty
add host: adds a host to the aggregate
remove host: removes a host from the aggregate
- Hosts
start host maintenance (or evacuate-host): disallow a host to serve API requests and migrate instances to other hosts of the aggregate
stop host maintenance: (or rebalance-host): put the host back into operational mode, migrating instances back onto that host
理解:对于Aggregate操作有:列出、创建、显示信息、更新、设置metadaa、删除、添加主机、移除主机。对于host的操作有:开启维护主机、停止维护主机。
]]>Nova is focusing on doing an awesome job of its core mission. This document aims to clarify that core mission.
This is a living document to help record where we agree about what Nova should and should not be doing, and why. Please treat this as a discussion of interesting, and hopefully useful, examples. It is not intended to be an exhaustive policy statement.
理解:文档的主要内容是,理清Nova的核心使命。
Our mission statement starts with:
To implement services and associated libraries to provide massively scalable, on demand, self service access to compute resources.Our official mission statement also includes the following examples of compute resources: bare metal, virtual machines, and containers. For the full official mission statement see: http://governance.openstack.org/reference/projects/nova.html#mission
This document aims to help clarify what the mission statement means.
理解:Nova的核心任务是:实现大规模可扩展地、按需地、自助地访问计算资源的服务和相关库。
Nova is all about access to compute resources. This section looks at the types of compute resource Nova works with.
理解:Nova的一切都是和访问计算资源相关的。这节讲讲Nova主要能够在哪些计算资源上工作。
Nova was originally focused purely on providing access to virtual servers running on a variety of different hypervisors. The majority of users use Nova only to provide access to virtual servers from a single hypervisor, however, its possible to have a Nova deployment include multiple different types of hypervisors, while at the same time offering containers and bare metal servers.
理解:Nova支持在不同hypervisor的虚拟服务器的访问,同时,也支持容器和裸金属服务器
The Nova API is not a good fit for a lot of container use cases. The Magnum project intends to deliver a good container experience built on top of Nova.
Nova allows you to use containers in a similar way to how you would use on demand virtual machines. We want to maintain this distinction, so we maintain the integrity and usefulness of the existing Nova API.
For example, Nova is not designed to spin up new containers for every apache request, nor do we plan to control what goes on inside containers. They get the same metadata provided to them as virtual machines, to do with as they see fit.
理解:
Ironic project has been pioneering the idea of treating physical machines in a similar way to on demand virtual machines.
Nova’s driver is able to allow a multi-tenant cloud style use of Ironic controlled resources.
While currently there are operations that are a fundamental part of our virtual machine abstraction that are not currently available in ironic, such as attaching iSCSI volumes, it does not fundamentally change the semantics of our API, and as such is a suitable Nova driver. Moreover, it is expected that gap with shrink over time.
理解:Ironic项目项目用来统一不同物理机器的访问接口。
Our goal for the Nova API to provide a consistent abstraction to access on demand compute resources. We are not aiming to expose all features of all hypervisors. Where the details of the underlying hypervisor leak through our APIs, we have failed in this goal, and we must work towards better abstractions that are more interoperable. This is one reason why we put so much emphasis on the use of Tempest in third party CI systems.
The key tenant of driver parity is that if a feature is supported in a driver, it must feel the same to users, as if they where using any of the other drivers that also support that feature. The exception is that, if possible for widely different performance characteristics, but the effect of that API call must be identical.
Following on from that, should a feature only be added to one of the drivers, we must make every effort to ensure another driver could be implemented to match that behavior.
Its important that drivers support enough features, so the API actually provides a consistent abstraction. For example, being unable to create a server or delete a server, would severely undermine that goal. In fact, Nova only ever manages resources it creates.
理解:
Nova is widely used in production. As such we need to respect the needs of our existing users. At the same time we need evolve the current code base, including both adding and removing features.
This section outlines how we expect people to upgrade, and what we do to help existing users that upgrade in the way we expect.
理解:升级权衡兼容性和新功能增删
Our upgrade plan is to concentrate on upgrades from N-1 to the Nth release. So for someone running juno, they would have to upgrade to kilo before upgrading to liberty. This is designed to balance the need for a smooth upgrade, against having to keep maintaining the compatibility code to make that upgrade possible. We talk about this approach as users consuming the stable branch.
In addition, we also support users upgrading from the master branch. Technically, between any two between any two commits within the same release cycle. In certain cases, when crossing release boundaries, you must upgrade to the stable branch, before then upgrading to the tip of master. This is to support those that are doing some level of “Continuous Deployment” from the tip of master into production. Many of the public cloud provides running OpenStack use this approach so they are able to get access to bug fixes and features they work on into production sooner.
This becomes important when you consider reverting a commit that turns out to have been bad idea. We have to assume any public API change may have already been deployed into production, and as such cannot be reverted. In a similar way, a database migration may have been deployed.
Any commit that will affect an upgrade gets the UpgradeImpact tag added to the commit message, so there is no requirement to wait for release notes.
理解:
UpgradeImpack
的标签As a community we are aiming towards a smooth upgrade process, where users must be unaware you have just upgraded your deployment, except that there might be additional feature available and improved stability and performance of some existing features.
We don’t ever want to remove features our users rely on. Sometimes we need to migrate users to a new implementation of that feature, which may require extra steps by the deployer, but the end users must be unaffected by such changes. However there are times when some features become a problem to maintain, and fall into disrepair. We aim to be honest with our users and highlight the issues we have, so we are in a position to find help to fix that situation. Ideally we are able to rework the feature so it can be maintained, but in some rare cases, the feature no longer works, is not tested, and no one is stepping forward to maintain that feature, the best option can be to remove that feature.
When we remove features, we need warn users by first marking those features as deprecated, before we finally remove the feature. The idea is to get feedback on how important the feature is to our user base. Where a feature is important we work with the whole community to find a path forward for those users.
理解:
Nova aims to provide a highly interoperable and stable REST API for our users to get self-service access to compute resources.
理解:Nova提供一个可操作性强、稳定的REST API
Nova API current has some APIs that are now (in kilo) mostly just a proxy to other OpenStack services. If it were possible to remove a public API, these are some we might start with. As such, we don’t want to add any more.
The first example is the API that is a proxy to the Glance v1 API. As Glance moves to deprecate its v1 API, we need to translate calls from the old v1 API we expose, to Glance’s v2 API.
The next API to mention is the networking APIs, in particular the security groups API. If you are using nova-network, Nova is still the only way to perform these network operations. But if you use Neutron, security groups has a much richer Neutron API, and if you use both Nova API and Neutron API, the miss match can lead to some very unexpected results, in certain cases.
Our intention is to avoid adding to the problems we already have in this area.
理解:目前Nova API中有一些API只是其他服务的代理,以后不会再添加类似的代理服务了。
Nova is a low level infrastructure API. It is plumbing upon which richer ideas can be built. Heat and Magnum being great examples of that.
While we have some APIs that could be considered orchestration, and we must continue to maintain those, we do not intend to add any more APIs that do orchestration.
理解:Nova是一个低层次基础设施的API,不会有过多的编排的内容。
Nova aims to focus on making a great API that is highly interoperable across all Nova deployments.
We have historically done a very poor job of implementing and maintaining compatibility with third party APIs inside the Nova tree.
As such, all new efforts should instead focus on external projects that provide third party compatibility on top of the Nova API. Where needed, we will work this those projects to extending the Nova API such that its possible to add that functionality on top of the Nova API. However, we do not intend to add API calls for those services to persist third party API specific information in the Nova database. Instead we want to focus on additions that enhance the existing Nova API.
理解:Nova正在重视第三方API,尤其重视可以提高现有API的第三方API。
Our mission includes the text “massively scalable”. Lets discuss what that means.
Nova has three main axises of scale: Number of API requests, number of compute nodes and number of active instances. In many cases the number of compute nodes and active instances are so closely related, you rarely need to consider those separately. There are other items, such at the number of tenants, and the number of instances per tenant. But, again, these are very rarely the key scale issue. Its possible to have a small cloud with lots of requests for very short lived VMs, or a large cloud with lots of longer lived VMs. These need to scale out different components of the Nova system to reach their required level of scale.
Ideally all Nova components are either scaled out to match the number of API requests and build requests, or scaled out to match the number of running servers. If we create components that have their load increased relative to both of these items, we can run into inefficiencies or resource contention. Although it is possible to make that work in some cases, this should always be considered.
We intend Nova to be usable for both small and massive deployments. Where small involves 1-10 hypervisors and massive deployments are single regions with greater than 10,000 hypervisors. That should be seen as our current goal not an upper limit.
There are some features that would not scale well for either the small scale or the very large scale. Ideally we would not accept these features, but if there is a strong case to add such features, we must work hard to ensure you can run without that feature at the scale you are required to run.
理解:
Currently Nova focuses on providing on-demand compute resources in the style of classic Infrastructure-as-a-service clouds. A large pool of compute resources that people can consume in a self-service way.
Nova is not currently optimized for dealing with a larger number of requests for compute resources compared with the amount of compute resource thats currently available. We generally assume a level of spare capacity is maintained for future requests. This is needed for users that want to quickly scale out, and extra capacity becomes available again as users scale in. While spare capacity is also not required, we are not optimizing for a system that aims to run at 100% capacity at all times. As such our quota system is more focused on limiting the current level of resource usage, rather than ensuring a fair balance of resources between all incoming requests. This doesn’t exclude adding features to support making a better use of spare capacity, such as “spot instances”.
There have been discussions around how to change Nova to work better for batch job processing. But the current focus is on how to layer such an abstraction on top of the basic primitives Nova currently provides, possibly adding additional APIs where that makes good sense. Should this turn out to be impractical, we may have to revise our approach.
理解:Nova重视现有水平的资源利用,目前比较重视作为Iaas的API。
Nova does not plan on creating its own packaging or deployment systems.
Our CI infrastructure is powered by Devstack. This can also be used by developers to test their work on a full deployment of Nova.
We do not develop any deployment or packaging for production deployments. Being widely adopted by many distributions and commercial products, we instead choose to work with all those parties to ensure they are able to effectively package and deploy Nova.
理解:
Nova is built on a shared-nothing, messaging-based architecture. All of the major nova components can be run on multiple servers. This means that most component to component communication must go via message queue. In order to avoid blocking each component while waiting for a response, we use deferred objects, with a callback that gets triggered when a response is received.
Nova建立在一个无共享,基于消息的架构。所有的nova主要组件都可以运行在不同的服务器。这就意味着大多数组件之间的通信必须通过消息队列。为了避免每个组件在等待响应时的阻塞,我们是用deferred对象,当一个响应接收时会触发相应的回调。
Nova recently moved to using a sql-based central database that is shared by all components in the system. The amount and depth of the data fits into a sql database quite well. For small deployments this seems like an optimal solution. For larger deployments, and especially if security is a concern, nova will be moving towards multiple data stores with some kind of aggregation system.
Nova最近正在迁移到一个基于SQL的中心数据库,系统中所有组件都共享一个数据库。大量的深度的数据非常适合存放在SQL数据库中。对于小型部署,这是一个最佳方案。对于大型部署,特别是非常看重安全的话,nova也将会进一步搬到不同集成系统中的多种数据库。
下面将看到两种不同的架构:
While all services are designed to be horizontally scalable, you should have significantly more computes then anything else.
由于所有的服务都被设计为横向扩展的,你只需要更多的计算机就可以了。
]]>OpenStack起于RakeSpace和NASA的合作,相关知识涉及到了云计算和虚拟化,体系结构上来看OpenStack是处于Iaas层,目前来看,还是处于追随AWS的阶段。OpenStack的子项目从新项目、孵化项目最终发展成为核心及集成项目。
作者比较文艺,对于文档的重要性是这么描述的:
每一个项目,每一个模块,甚至每一行代码都有着自己的故事,这些故事都应该被留存在自己的历史档案里,而我们大部分的时间都只是他们的过客而已。
另外,还有一个重要的东西就是Code Review,核心思想:
一旦一个问题被充分地描述了他的细节,那么解决方法也是显而易见的。
OpenStack的代码质量保证体系大概就是以下几点:
其实,整个过程还是比较清晰的,现在只是大致了解一下过程,后面真正开发的时候亲身体验一下再深入理解。最后,又介绍了一些关于贡献代码、文档、Review之类的一些说明,其中,Feature的贡献,需要先在Blueprint提出来一些想法和设计,然后通过后再在spec里详细描述一些细节。
对于阅读代码,也给了一个基本的思路,因为代码的脉络比较复杂,所以推荐先阅读setup.cfg,然后从entry point入手,逐个功能、服务去突破。OpenStack牵扯到的背景知识,主要有Python、Linux、网络基础、虚拟化、Git,除了虚拟化外,其他掌握的还行。所以,后面多看看虚拟化方面的内容。
对于OpenStack来说,虚拟化相关内容是非常核心的内容,自然本书的第三章独立出来介绍虚拟化的知识。
虚拟化的按照实现方式来说,主要分为Hypervisior模型和Hosted模型。
按照平台类型来说,则大致分为完全虚拟化和类虚拟化。
对于虚拟化相关知识,有参考了一些其他资料:
《虚拟化技术原理与实现》
《系统虚拟化原理与实现》
Understanding Full Virtualization, Paravirtualization, and Hardware Assist(翻译)
OpenStack is a cloud operating system that controls large pools of compute, storage, and networking resources throughout a datacenter, all managed through a dashboard that gives administrators control while empowering their users to provision resources through a web interface.
正如OpenStack官方定义的那样,OpenStack是一个云操作系统,他管理这大量的计算、存储、网络资源,也提供了一个dashborad让管理员通过Web接口方便的管理资源。目前的版本已经是K版了,在看了OpenStack Kilo Overview Demo的视频后,对OpenStack有了最初的了解。
Compute
Provision and manage large networks of virtual machines.
The OpenStack cloud operating system enables enterprises and service providers to offer on-demand computing resources, by provisioning and managing large networks of virtual machines. Compute resources are accessible via APIs for developers building cloud applications and via web interfaces for administrators and users. The compute architecture is designed to scale horizontally on standard hardware, enabling the cloud economics companies have come to expect.
几个关键信息:按需提供计算资源、提供API给开发者、提供Web接口给用户、横向扩展。然后又专门介绍了OpenStack的灵活的架构,它支持各种不同的硬件,比如普通的计算机可以,高性能计算机也可以;也支持不同的软件,比如支持各种不同的虚拟化技术,比如KVM、XEN、LXC等等。
Storage
Object and block storage for use with servers and applications.
In addition to traditional enterprise-class storage technology, many organizations now have a variety of storage needs with varying performance and price requirements. OpenStack has support for both Object Storage and Block Storage, with many deployment options for each depending on the use case.
Object Storage is ideal for cost effective, scale-out storage. It provides a fully distributed, API-accessible storage platform that can be integrated directly into applications or used for backup, archiving and data retention. Block Storage allows block devices to be exposed and connected to compute instances for expanded storage, better performance and integration with enterprise storage platforms, such as NetApp, Nexenta and SolidFire.
几个关键信息,提供了对象存储和块存储,对象存储有高效的成本,横向扩展性,块存储则为了更好的扩容、更好的性能以及和企业其他存储平台的集成。
Network
Pluggable, scalable, API-driven network and ip management.
Today’s datacenter networks contain more devices than ever before servers, network equipment, storage systems and security appliances — many of which are further divided into virtual machines and virtual networks. The number of IP addresses, routing configurations and security rules can quickly grow into the millions. Traditional network management techniques fall short of providing a truly scalable, automated approach to managing these next-generation networks. At the same time, users expect more control and flexibility with quicker provisioning.
OpenStack Networking is a pluggable, scalable and API-driven system for managing networks and IP addresses. Like other aspects of the cloud operating system, it can be used by administrators and users to increase the value of existing datacenter assets. OpenStack Networking ensures the network will not be the bottleneck or limiting factor in a cloud deployment and gives users real self service, even over their network configurations.
关键信息:现今数据中心的设备越来越多,配置也非常多,需要一个可扩展、自动化的管理下一代网络,同时用户也希望仅通过快速配置,掌控更多,更灵活。所以就有OpenStack Networking了,一个用于管理网络和IP的可插拔、可扩展、API驱动的系统。
一年多的时间,基本都在中国第一学府——北京大学。这里有未名湖畔的波光粼粼,也有博雅塔下的春风垂柳;有艺园吃腻很久的炒菜米饭,也有小白房冒菜不限量的芝麻酱和鳕鱼培根夹馍;有农园清淡爽口的菜肴和炸酱面,也有畅春园改善伙食的麻辣香锅。总之一切都还美好,只是有老校医院实验室中永远调不完的bug后,其他的一切又稍显黯淡。
周末的闲暇时光,也会去各个古迹去探索一番,从长城的一砖一瓦中,猜得出北京千年的文化的底蕴;故宫里的金瓦红墙,看得到旧朝的兴盛衰亡;圆明园的断壁残垣,见证着过往苍白的历史。这些古迹除了斑驳的历史以外,还有一个共同点,那就是人多。有时候想想也会觉得惊讶,北京的各个景点在任何的时候都充斥着络绎不绝的游客。
静静地坐落在京西的古刹龙泉寺,却又是以另一种沉静展现出新的世界。听说,行走在寺中的任何一个僧人都可能清华北大拿到全奖留美offer的才俊,也有人戏称这里是清华北大的分校。 一叶一菩提,一沙一世界,在这新的世界里,真的会让人有种沉寂身心,潜学求知的心境。

一直觉得,来到一个城市,只有见过了她的四季才算来过。当我真真切切地感受到北京的春夏秋冬后,这座城的全部已经烙入我的心中:春天凌乱纷飞的迷乱柳絮,夏日说来就来的疯狂暴雨,秋季萧瑟苍凉的香山红叶,冬夜刺骨冻人的凛冽大风。当你在这时,她分明的四季演绎出这座城池的精彩,当你不在时,她同样会把更精彩的四季展示给其他千千万万的北漂人。
最开始的几个月,住在苏州街,这个有几分诗意的名字,却和她的忙碌一点儿也不相符。每天早上,看着疾步似飞的北漂人在梦想路上暴走。从酒店到北大的路也是分外的熟悉。晴好的夜晚,也会在中关村这片跑个几公里。相信明朝的太监们怎么也想不到,如今他们的故里已经成为中国IT的中心。中关村的环形天桥矗立在密密麻麻的互联网公司之上,下面也永远是车水马龙。行人满脸匆忙地在桥上穿梭,好像整个世界都按了快进。当你夜晚静静的驻足桥上,零星的装点竟然有种豪华的感觉。看着底下走走停停的车辆,别有一番说不出的滋味。

转眼间已经在北京待了一年时间,渐渐地体会到了“漂”的内涵。不是在风中飘摇的飘,而是在水上漂泊的漂,而北京浑厚丰富地资源就像浩瀚无边的海洋。这座古韵十足而深藏活力的城池,永远都在这里等待着成千上万的创业者,成功失败都在这里发生。有的时候,你需要在让人喘不过气的雾霾中,奋力的挣扎去呼吸,去成长,当然了,让人喘不过气的东西也不仅仅是雾霾。北京承载了太多人的期望,或为生计,或为梦想。他们在这里,生根发芽,这里有太多太多的竞争者,也有太多太多的机会。
晚上,就要离开北京了,那么:再见,北京!
]]>最近一段时间,总是有一些发数据包、抓数据包之类的需求,主要是需要定制以太网MAC层的源MAC地址、目的MAC地址、协议类型之类的东西。
之前实现这些都是用Winpcap实现的,然后用某基础类库去实现界面,实在是受不了“某基础类库”庞大冗余的结构了。这时,看到了PyQt,整个人一下清爽起来。所以,Packet Assistant就诞生了。
总而言之,言而总之:
Packet Assistant是一个简单的发包、抓包工具。
Python底下其实有很多的GUI库,比如这里提到的一些,因为之前对C++的Qt比较熟,所以,比较能够理解Qt的设计思想,用PyQt的时候,也觉得还好,和原生Qt没什么差,主要是考虑了需要一个类似Qt Designer的东西快速搭界面。所以就选了PyQt了。
Python网络抓包的库,Winpcap官网的友情链接提到了这个库,比起PyPcap,Pcapy的例子、支持感觉更好些。
Enjoy it! :+1:
]]>大致单步跟了下Spring IOC的初始化过程,整个脉络很庞大,初始化的过程主要就是读取XML资源,并解析,最终注册到Bean Factory中:
在完成初始化的过程后,Bean们就在BeanFactory中蓄势以待地等调用了。下面通过一个具体的例子,来详细地学习一下初始化过程,例如当加载下面一个bean:
1 | <bean id="XiaoWang" class="com.springstudy.talentshow.SuperInstrumentalist"> |
加载时需要读取、解析、注册bean,这个过程具体的调用栈如下所示:
下面对每一步的关键的代码进行详细分析:
保存配置位置,并刷新
在调用ClassPathXmlApplicationContext后,先会将配置位置信息保存到configLocations,供后面解析使用,之后,会调用AbstractApplicationContext
的refresh方法进行刷新:
1 | public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, |
创建载入BeanFactory
1 | protected final void refreshBeanFactory() throws BeansException { |
创建XMLBeanDefinitionReader
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) |
创建处理每一个resource
1 | public int loadBeanDefinitions(String location, Set<Resource> actualResources) |
处理XML每个元素
1 | protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { |
解析和注册bean
1 | protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { |
本步骤中,通过parseBeanDefinitionElement
将XML的元素解析为BeanDefinition
,然后存在BeanDefinitionHolder
中,然后再利用BeanDefinitionHolder
将BeanDefinition
注册,实质就是把BeanDefinition
的实例put进BeanFactory
中,和后面将详细的介绍解析和注册过程。
处理每个Bean的元素
1 | public AbstractBeanDefinition parseBeanDefinitionElement( |
处理属性的值
1 | public Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) { |
1 | public static void registerBeanDefinition( |
注册过程中,最核心的一句就是:this.beanDefinitionMap.put(beanName, beanDefinition)
,也就是说注册的实质就是以beanName为key,以beanDefinition为value,将其put到HashMap中。
当完成初始化IOC容器后,如果bean没有设置lazy-init(延迟加载)属性,那么bean的实例就会在初始化IOC完成之后,及时地进行初始化。初始化时会先建立实例,然后根据配置利用反射对实例进行进一步操作,具体流程如下所示:
创建bean的实例
创建bean的实例过程函数调用栈如下所示:
注入bean的属性
注入bean的属性过程函数调用栈如下所示:
在创建bean和注入bean的属性时,都是在doCreateBean函数中进行的,我们重点看下:
1 | protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, |
理解了以上两个过程,我们就可以自己实现一个简单的Spring框架了。于是,我根据自己的理解实现了一个简单的IOC框架Simple Spring,有兴趣可以看看。
]]>Web日志由Web服务器产生,比如Nginx、Apache等。例如一条Nginx的日志格式可能是这样的:
222.68.172.190 - - [18/Sep/2013:06:49:57 +0000] “GET /images/my.jpg HTTP/1.1” 200 19939
“http://www.angularjs.cn/A00n" “Mozilla/5.0 (Windows NT 6.1)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36”
可以拆解为以下8个变量
remote_addr: 记录客户端的ip地址, 222.68.172.190
remote_user: 记录客户端用户名称, –
time_local: 记录访问时间与时区, [18/Sep/2013:06:49:57 +0000]
request: 记录请求的url与http协议, “GET /images/my.jpg HTTP/1.1″
status: 记录请求状态,成功是200, 200
body_bytes_sent: 记录发送给客户端文件主体内容大小, 19939
http_referer: 用来记录从那个页面链接访问过来的, “http://www.angularjs.cn/A00n”
http_user_agent: 记录客户浏览器的相关信息, “Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36″
思路一:利用awk提取时间,并做字符串比较
1 | # 在OSX环境下,需要安装coreutils保证`date`等指令正常使用 |
思路二:利用grep的正则匹配,捕获满足条件的时刻
1 | cat log | grep -E "19/Sep/2013:06:2[4-9]|19/Sep/2013:06:3[0-3]" |
思路三:使用sed的地址(时刻必须出现)
1 | cat log | sed -n '/19\/Sep\/2013:06:24/,//19\/Sep\/2013:06:33/p' |
1 | # cat log | grep xxxapi | wc -l |
海量Web日志分析 用Hadoop提取KPI统计指标
shell脚本每天自动统计网站访问日志
如何用Shell截取nginx最近10分钟的日志
如何用awk从日志文件中找到时间范围的数据
Shell
比较熟悉了,但是遇到一些不太常用,但是却很有用的命令,还得搜个半天。所以,这几天准备把常用的指令重新过一下,然后再把基础的Shell
流程控制、awk
的操作等常用命令系统地学习一下,一方面作为总结,另一方面也作为一个Cheat Sheet,时不时的补充一下也是极好的,: )。传参$n
- 传递的第n个参数$#
- 传递的参数个数$*
- 所有参数$@
- 所有参数,加引号为字符串数组
1 | # var.sh arg1 arg2 arg3 |
if条件控制
1 | a=10 |
循环控制
1 | arr=("hello" "bash" "shell") |
_head/tail_
1 | # 前5行 |
_sort_
1 | # 忽略起始空格(b)数值(n)去重(u)逆序(r)按照:分隔(-t ':')的第二列(-k 2)排序 |
_grep_
1 | # 在当前文件夹的所有文件递归(-r)搜索main |
_wc_
1 | # 打印所有.c文件的行数 |
并发优化的ArrayList。用CopyOnWrite策略,在修改时先复制一个快照来修改,改完再让内部指针指向新数组。
因为对快照的修改对读操作来说不可见,所以只有写锁没有读锁,加上复制的昂贵成本,典型的适合读多写少的场景。如果更新频率较高,或数组较大时,还是Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全更好。
增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。
]]>理解这些指令,觉得最重要的是理解Git的内部原理,比如Git的分布式版本控制,分清楚工作区、暂存区、版本库,还有就是理解Git跟踪并管理的是修改,而非文件。
1 | $ git config --global user.name "Your Name" |
git tracked的是修改,而不是文件
1 | #将“当前修改”移动到暂存区(stage) |
1 | $ git status |
1 | # 放弃工作区修改 |
1 | $ git reflog |
1 | $ rm file.name |
1 | $ git remote add origin git@github.com:michaelliao/learngit.git |
1 | $ git clone https://github.com/Yikun/yikun.github.com.git path |
1 | # 产看当前分支 |
1 | $ git tag 0.1.1 |
1 | // 1. 添加上游 |
A specialized Map implementation for use with enum type keys. All of the keys in an enum map must come from a single enum type that is specified, explicitly or implicitly, when the map is created. Enum maps are represented internally as arrays. This representation is extremely compact and efficient.
EnumMap是是一种键为枚举类型的特殊的Map实现。所有的Key也必须是一种枚举类型,EnumMap是使用数组来实现的。
1 | EnumMap<Course, String> map = new EnumMap<Course, String>(Course.class); |
输出结果为:
1 | ONE: 政治 |
其具体实现的结构如下图所示:
put方法通过key的ordinal将值存储到对应的地方,get方法则根据key的ordinal获取对应的值。
1 | public V put(K key, V value) { |
EnumMapIterator的迭代这样实现的:
1 | public boolean hasNext() { |
通过hasNext跳过空的数组,也就是说,保证了遍历顺序与Enum中key的先后顺序一致。
有一个函数f,可以等概率产生0,1,写一个函数g,可以等概率地产生0~7中的数。
于是,我写出了如下一个错误的解法:
f()+f()<<1+f()<<2
而正确的应该是:
f() + (f()<<1) + (f()<<2)
之前一直有误解,而实际情况是是移位操作符的优先级低于加减法的优先级。为了验证和加深记忆,又写了一个小测试:
1 |
|
输出为:
1 | 1<<1+2<<1: 16 |
其中编译的时候,也有一个友情提示:
1 | main.c:4:36: warning: operator '<<' has lower precedence than '+'; '+' will be |
最后,
]]>希望自己永远保持一颗怀疑的心。
Resizable-array implementation of the Deque interface. Array deques have no capacity restrictions; they grow as necessary to support usage. They are not thread-safe; in the absence of external synchronization, they do not support concurrent access by multiple threads. Null elements are prohibited. This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.
]]>以循环数组实现的双向Queue。大小是2的倍数,默认是16。
普通数组只能快速在末尾添加元素,为了支持FIFO,从数组头快速取出元素,就需要使用循环数组:有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,如果已到数组空间的末尾,则将元素循环赋值到数组0,同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。
A NavigableSet implementation based on a TreeMap. The elements are ordered using their natural ordering, or by a Comparator provided at set creation time, depending on which constructor is used.This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).
TreeSet是基于TreeMap实现的,也非常简单,同样的只是用key及其操作,然后把value置为dummy的object。
1 | TreeSet<String> tset = new TreeSet<String>(); |
其具体的结构是:
利用TreeMap的特性,实现了set的有序性(通过红黑树实现)。
Hash table and linked list implementation of the Set interface, with predictable iteration order. This implementation differs from HashSet in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is the order in which elements were inserted into the set (insertion-order). Note that insertion order is not affected if an element is re-inserted into the set. (An element e is reinserted into a set s if s.add(e) is invoked when s.contains(e) would return true immediately prior to the invocation.)
LinkedHashSet是基于HashMap和双向链表的实现。
1 | LinkedHashSet<String> lset = new LinkedHashSet<String>(); |
利用链表来记录,保证了迭代输出的有序性。其具体结构如下所示:
可以看出,其实现基本和LinkedHashMap一样。
1 | public class LinkedHashSet<E> |
从继承关系来看就知道LinkedHashMap的实现非常简单,就是集成HashSet的接口,并且在构造时调用的是:
1 | HashSet(int initialCapacity, float loadFactor, boolean dummy) { |
因此,结构也便是如HashSet于HashMap一样,LinkedHashSet也便如LinkedHashMap一样,只是将Value做了一个dummy的object。
This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.
HashSet是基于HashMap来实现的,操作很简单,更像是对HashMap做了一次“封装”,而且只使用了HashMap的key来实现各种特性,我们先来感性的认识一下这个结构:
1 | HashSet<String> set = new HashSet<String>(); |
其大致的结构是这样的:
1 | private transient HashMap<E,Object> map; |
map
是整个HashSet的核心,而PRESENT
则是用来造一个假的value来用的。
1 | public boolean add(E e) { |
基本操作也非常简单,就是调用HashMap的相关方法,其中value就是之前那个dummy的Object。所以,只要了解#7 HashMap的实现就可以了。
基于最小堆(小根堆)的topn算法
基于堆实现的优先级队列:PriorityQueue 解决 Top K 问题
Java优先队列(PriorityQueue)示例
JDK源码研究PriorityQueue(优先队列)
优先级队列:PriorityQueue
A Red-Black tree based NavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.
This implementation provides guaranteed log(n) time cost for the containsKey, get, put and remove operations. Algorithms are adaptations of those in Cormen, Leiserson, and Rivest’s Introduction to Algorithms.
之前已经学习过HashMap和LinkedHashMap了,HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序,而如果我们希望Map可以保持key的大小顺序的时候,我们就需要利用TreeMap了。
1 | TreeMap<Integer, String> tmap = new TreeMap<Integer, String>(); |
其大致的结构如下所示:
使用红黑树的好处是能够使得树具有不错的平衡性,这样操作的速度就可以达到log(n)的水平了。具体红黑树的实现不在这里赘述,可以参考数据结构之红黑树、wikipedia-红黑树等的实现。
Associates the specified value with the specified key in this map.If the map previously contained a mapping for the key, the old value is replaced.
如果存在的话,old value被替换;如果不存在的话,则新添一个节点,然后对做红黑树的平衡操作。
1 | public V put(K key, V value) { |
get函数则相对来说比较简单,以log(n)的复杂度进行get
1 | final Entry<K,V> getEntry(Object key) { |
TreeMap是如何保证其迭代输出是有序的呢?其实从宏观上来讲,就相当于树的中序遍历(LDR)。我们先看一下迭代输出的步骤
1 | for(Entry<Integer, String> entry : tmap.entrySet()) { |
根据The enhanced for statement,for语句会做如下转换为:
1 | for(Iterator<Map.Entry<String, String>> it = tmap.entrySet().iterator() ; tmap.hasNext(); ) { |
在it.next()的调用中会使用nextEntry调用successor
这个是过的后继的重点,具体实现如下:
1 | static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { |
怎么理解这个successor呢?只要记住,这个是中序遍历就好了,L-D-R。具体细节如下:
a. 空节点,没有后继
b. 有右子树的节点,后继就是右子树的“最左节点”
c. 无右子树的节点,后继就是该节点所在左子树的第一个祖先节点
a.好理解,不过b, c,有点像绕口令啊,没关系,上图举个例子就懂了!
有右子树的节点,节点的下一个节点,肯定在右子树中,而右子树中“最左”的那个节点则是右子树中最小的一个,那么当然是右子树的“最左节点”,就好像下图所示:
无右子树的节点,先找到这个节点所在的左子树(右图),那么这个节点所在的左子树的父节点(绿色节点),就是下一个节点。
TreeMap (Java Platform SE 8)
浅谈算法和数据结构: 九 平衡查找树之红黑树
Java提高篇(二七)—–TreeMap
数据结构之红黑树
wikipedia-红黑树
Red-Black Trees
How TreeMap searches successor for given Entry?
以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。
按下标访问元素–get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组大小的一半,会从末尾移起)。
插入、删除元素时修改前后节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作–add(), addFirst(),removeLast()或用iterator()上的remove()能省掉指针的移动。
LinkedList是一个简单的数据结构,与ArrayList不同的是,他是基于链表实现的。
Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).
1 | LinkedList<String> list = new LinkedList<String>(); |
结构也相对简单一些,如下图所示:
1 | public E set(int index, E element) { |
这两个函数都调用了node
函数,该函数会以O(n/2)的性能去获取一个节点,具体实现如下所示:
1 | Node<E> node(int index) { |
就是判断index是在前半区间还是后半区间,如果在前半区间就从head搜索,而在后半区间就从tail搜索。而不是一直从头到尾的搜索。如此设计,将节点访问的复杂度由O(n)变为O(n/2)。
关于Java集合的小抄中是这样描述的:
以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。
按数组下标访问元素–get(i)/set(i,e) 的性能很高,这是数组的基本优势。
直接在数组末尾加入元素–add(e)的性能也高,但如果按下标插入、删除元素–add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。
然后再来学习一下官方文档:
Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
ArrayList是一个相对来说比较简单的数据结构,最重要的一点就是它的自动扩容,可以认为就是我们常说的“动态数组”。
来看一段简单的代码:
1 | ArrayList<String> list = new ArrayList<String>(); |
在执行这四条语句时,是这么变化的:
其中,add
操作可以理解为直接将数组的内容置位,remove
操作可以理解为删除index为0的节点,并将后面元素移到0处。
当我们在ArrayList中增加元素的时候,会使用add
函数。他会将元素放到末尾。具体实现如下:
1 | public boolean add(E e) { |
我们可以看到他的实现其实最核心的内容就是ensureCapacityInternal
。这个函数其实就是自动扩容机制的核心。我们依次来看一下他的具体实现
1 | private void ensureCapacityInternal(int minCapacity) { |
也就是说,当增加数据的时候,如果ArrayList的大小已经不满足需求时,那么就将数组变为原长度的1.5倍,之后的操作就是把老的数组拷到新的数组里面。例如,默认的数组大小是10,也就是说当我们add
10个元素之后,再进行一次add时,就会发生自动扩容,数组长度由10变为了15具体情况如下所示:
Array的put和get函数就比较简单了,先做index检查,然后执行赋值或访问操作:
1 | public E set(int index, E element) { |
1 | public E remove(int index) { |
注释很清楚:
Removes the element at the specified position in this list. Shifts any subsequent elements to the left (subtracts one from their indices).
之前,在LeetCode上看到一个LRU Cache实现的题目,题目描述是这样的:
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set.
get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
set(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
简单的说,就是保证基本的get和set的功能的同时,还要保证最近访问(get或put)的节点保持在限定容量的Cache中,如果超过容量则应该把LRU(近期最少使用)的节点删除掉。
那么我们思考一个问题:如何设计实现一个LRU Cache?
那么,我们可能需要使用类似这样的数据结构去实现这个LRU Cache:
这不就是LinkedHashMap吗!
这样做的好处是,get
和set
在不冲突的情况下可以保证O(1)的复杂度,同时,也可以通过双向链表来保证LRU的删除
和更新
操作也能保证O(1)的复杂度。
在学习了HashMap(#7 )和LinkedHashMap(#8 )后,是不是觉得这俩数据结构简直太适合做LRU Cache了!那么动手实现一下:
基于HashMap和双向链表的实现
1 | public class LRUCache { |
基于LinkedHashMap的实现
HashMap+双向链表?这不就是LinkedHashMap吗!
1 | public class LRUCache { |
在理解了#7 介绍的HashMap后,我们来学习LinkedHashMap的工作原理及实现。首先还是类似的,我们写一个简单的LinkedHashMap的程序:
1 | LinkedHashMap<String, Integer> lmap = new LinkedHashMap<String, Integer>(); |
运行结果是:
语文: 1
数学: 2
英语: 3
历史: 4
政治: 5
地理: 6
生物: 7
化学: 8
我们可以观察到,和HashMap的运行结果不同,LinkedHashMap的迭代输出的结果保持了插入顺序。是什么样的结构使得LinkedHashMap具有如此特性呢?我们还是一样的看看LinkedHashMap的内部结构,对它有一个感性的认识:
没错,正如官方文档所说:
Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).
LinkedHashMap是Hash表和链表的实现,并且依靠着双向链表保证了迭代顺序是插入的顺序。
在HashMap中提到了下面的定义:
1 | // Callbacks to allow LinkedHashMap post-actions |
LinkedHashMap继承于HashMap,因此也重新实现了这3个函数,顾名思义这三个函数的作用分别是:节点访问后、节点插入后、节点移除后做一些事情。
afterNodeAccess函数
1 | void afterNodeAccess(Node<K,V> e) { // move node to last |
就是说在进行put之后就算是对节点的访问了,那么这个时候就会更新链表,把最近访问的放到最后,保证链表。
afterNodeInsertion函数
1 | void afterNodeInsertion(boolean evict) { // possibly remove eldest |
如果用户定义了removeEldestEntry
的规则,那么便可以执行相应的移除操作。
afterNodeRemoval函数
1 | void afterNodeRemoval(Node<K,V> e) { // unlink |
这个函数是在移除节点后调用的,就是将节点从双向链表中删除。
我们从上面3个函数看出来,基本上都是为了保证双向链表中的节点次序或者双向链表容量所做的一些额外的事情,目的就是保持双向链表中节点的顺序要从eldest到youngest。
put
函数在LinkedHashMap中未重新实现,只是实现了afterNodeAccess
和afterNodeInsertion
两个回调函数。get
函数则重新实现并加入了afterNodeAccess
来保证访问顺序,下面是get
函数的具体实现:
1 | public V get(Object key) { |
值得注意的是,在accessOrder模式下,只要执行get或者put等操作的时候,就会产生structural modification
。官方文档是这么描述的:
A structural modification is any operation that adds or deletes one or more mappings or, in the case of access-ordered linked hash maps, affects iteration order. In insertion-ordered linked hash maps, merely changing the value associated with a key that is already contained in the map is not a structural modification. In access-ordered linked hash maps, merely querying the map with get is a structural modification.
不要犯了像ConcurrentModificationException with LinkedHashMap类似的问题。
总之,LinkedHashMap不愧是HashMap的儿子,和老子太像了,当然,青出于蓝而胜于蓝,LinkedHashMap的其他的操作也基本上都是为了维护好那个具有访问顺序的双向链表。:-)
Class LinkedHashMap
ConcurrentModificationException with LinkedHashMap
从本文你可以学习到:
- 什么时候会使用HashMap?他有什么特点?
- 你知道HashMap的工作原理吗?
- 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
- 你知道hash的实现吗?为什么要这样实现?
- 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
当我们执行下面的操作时:
1 | HashMap<String, Integer> map = new HashMap<String, Integer>(); |
运行结果是
政治: 5
生物: 7
历史: 4
数学: 2
化学: 8
语文: 1
英语: 3
地理: 6
发生了什么呢?下面是一个大致的结构,希望我们对HashMap的结构有一个感性的认识:
在官方文档中是这样描述HashMap的:
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
几个关键的信息:基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保证序不随时间变化。
在HashMap中有两个很重要的参数,容量(Capacity)和负载因子(Load factor)
- Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
- Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
简单的说,Capacity就是buckets的数目,Load factor就是buckets填满程度的最大比例。如果对迭代性能要求很高的话不要把capacity
设置过大,也不要把load factor
设置过小。当bucket填充的数目(即hashmap中元素的个数)大于capacity*load factor
时就需要调整buckets的数目为当前的2倍。
put函数大致的思路为:
具体代码的实现如下:
1 |
|
在理解了put之后,get就很简单了。大致思路如下:
具体代码的实现如下:
1 | public V get(Object key) { |
在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示:
在对hashCode()计算hash时具体实现是这样的:
1 | static final int hash(Object key) { |
可以看到这个函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。其中代码注释是这样写的:
Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
在设计hash函数时,因为目前的table长度n为2的幂,而计算下标的时候,是这样实现的(使用&
位操作,而非%
求余):
1 | (n - 1) & hash |
设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在n - 1为15(0x1111)时,其实散列真正生效的只是低4bit的有效位,当然容易碰撞了。
因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)
的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在JEP-180中,描述了这个问题:
Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class.
之前已经提过,在获取HashMap的元素时,基本分两步:
在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
因此在Java 8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题,在Java 8:HashMap的性能提升一文中有性能测试的结果。
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。resize的注释是这样描述的:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
大致意思就是说,当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:
因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
下面是代码的具体实现:
1 | final Node<K,V>[] resize() { |
我们现在可以回答开始的几个问题,加深对HashMap的理解:
1. 什么时候会使用HashMap?他有什么特点?
是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
2. 你知道HashMap的工作原理吗?
通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
4. 你知道hash的实现吗?为什么要这样实现?
在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)
,主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。
关于Java集合的小抄中是这样描述的:
以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。
插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。
JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。
在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。
当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。
取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。
iterator()时顺着哈希桶数组来遍历,看起来是个乱序
HashMap的工作原理
Java 8:HashMap的性能提升
JEP 180: Handle Frequent HashMap Collisions with Balanced Trees
ConurrentHashMap和Hashtable的区别
HashMap和Hashtable的区别
这Java官方的入门文档是这样描述集合的:
Collection(有时候也叫container)是一个简单的对象,它把多个元素组织成一个单元。集合可以用来存储、检索、操作、通信。通常情况下,集合代表了一个自然数据项,比如一组手牌(牌的集合)、邮件文件夹(邮件的集合)、电话目录(姓名到电话的映射)。如果你使用过Java或者其他语言,你应该很熟悉集合。
Collections Framework是一个用来表示和操作集合的统一的架构。集合的框架包括了:
- Interfaces:
这些是表示集合的抽象数据类型,接口允许集合完成操作,独立与其详细的实现。在面向对象的语言中,接口构成了体系架构;- Implementations:
这些是接口的具体实现。本质上,是一些可复用的数据结构;- Algorithms:
这些方法可以对接口实现的对象进行有用的计算,比如搜索、排序。这些算法是具有多态性的:也就是说,同样的方法可以用在合适的接口的不同实现。本质上,是一些可复用的函数。除了Java的集合框架,还有一些著名的集合框架的例子:比如C++的STL和Smalltalk的集合架构。从历史上来看,集合框架可能比较复杂,也可能有一些很陡峭的学习曲线。不过我们相信Java的集合框架会突破这样的传统,在这章你就可以自己学会。
Java的集合框架提供了一下优点:
- 减少编程的工作量:通过提供有用的数据结构和算法,集合框架能让你更专注的实现程序的核心功能,而不是去做一个底层的“管道工”。Java框架通过促进无关API的互操作性,使得你不用自己去实现不同API的适配
- 提高程序的速度与质量:集合框架提供了一些有用数据结构和算法的高性能、高质量的实现。每个接口的不同的实现也是可以互换的,所以程序可以通过切换集合来做一些调整。正因为你从实现数据结构的那些苦差事中脱离出来,你才可以有更多的实现去改善你自己程序的性能和质量
- 允许无关APIs的互操作:集合接口是API之间传递集合的一个“方言”,比如我的网络管理API有一个节点名的集合,而GUI工具需要一个列标题的集合,即使是分开实现它们,我们的APIs也可以无缝的接合。
- 省力地学习和使用新API:
这是另一个领先的优势,设计者和实现者没必要在每次都重新设计API的时候都“推倒重来”地实现集合,而是直接使用标准的集合接口就好了。- 促进软件的复用:符合标准集合接口的新数据结构本质上是可以复用的。对于操作这些新数据结构算法也是一样可以复用的。
因此,后面也便从接口、实现、算法几方面结合着代码和官方的文档学习总结一下。
在Java中所有的核心集合接口都是generic的
1 | public interface Collection<E> extends Iterable<E> {} |
也就是说在声明一个Collection的时候,应该指定一种类型。官方是这样解释原因的:
Specifying the type allows the compiler to verify (at compile-time) that the type of object you put into the collection is correct, thus reducing errors at runtime.
下面就来介绍一下几种接口:
在1.6版本开始,还有两种新的接口NavigableSet、NavigableMap。
A SortedMap/SortedSet extended with navigation methods reporting closest matches for given search targets.
提供诸如:
1 | //返回第一个大于e的元素 |
之类的“导航性质”的便捷操作。
以上便是Java集合框架与接口的相关内容。
]]>最近需要实现一个简单的定时器管理,背景是硬件只有一个定时中断器,每100ms给一个时钟中断,我们需要利用它及相关代码实现整个协议中的定时器管理。然后大概看了看Linux的定时器、Nginx的定时器、Tegine的定时器,把一些思路和收获记一下。
思路本身很简单,用硬件时钟计数,每次检查,有定时器该执行就调用就行了。
具体的来说就是
1 | if (jiffies < timeout) |
首先,遇到的第一个问题就是滴答数溢出的问题,在无符号数到达最大值时,会从0开始重新累加。
例如,我们用unsigned char
计数,那么滴答数会从0(0x00)->1(0x01)->....->254(0xFE)->255(0xFF)->0(0x00)->1(0x01)->...
如此循环计数。
那么问题出现了,在254时刻出现了一个A事件(+4后执行),即时间溢出为0x02,那么如果我们做判定时,很显然,在254滴答的时候就把2滴答(实际上我们指的是254+4=258)的事件执行了。
1 | if(254 < 2) //false |
其实问题的根本原因是事件发生滴答和当前时间滴答使用的不是一个参考系。然而在Linux代码实现中,利用无符号转有符号的巧妙方式解决了上面那个问题,linux/include/linux/jiffies.h:
1 | // time_after(a,b) returns true if the time a is after time b. |
用了这个宏以后,巧妙的一幕发生了
1 | if(char(254) < char(1)) //true => -2 < -1 |
然后呢,在Linux 3.12的一个patch中,又修改了下:
1 |
|
大致意思就是C语言标准里面,有符号整型的溢出在C语言的标准中是未定义的blabla但是减法不会溢出,所以我这个改法是正确的blabla。哦对了,于是,我们对比时间代码编程了下面:
1 | if(time_before(jiffies, timeout)) |
类似的Tegine/Nginx也是使用的类似的实现方法,而且有趣的是nginx也出现过类似的patch。
因为我们的定时器个数比较少,所以用有序链表(按照expire时间)就差不多了。插入、删除都是O(n),查找O(1);
在Linux使用的是双向链表实现的;
在Nginx的实现中,则使用的是红黑树,类似的每次都要log(n)去查找最小节点;
据说Tengine要使用四叉最小堆(4-heap)去改进,在查找的时候能O(1),其他操作可以O(log(n)),不过我看tengine -2.1.0的代码中,还是用的红黑树实现的。
How does linux handle overflow in jiffies?
jiffies溢出与时间先后比较
linux内核计算时间差以及jiffies溢出
nginx学习 - timer
淘宝 Tengine易运维的高性能Nginx服务器
1 | int i=1; |
1 | mov eax,dword ptr [i] |
1 | int i=1; |
1 | mov eax,dword ptr [i] |
很清晰了。: )
]]>sudo apt-get install git
sudo adduser git
cat id_rsa.pub >> /home/git/.ssh/authorized_keys
sudo git init –bare sample.git
sudo chown -R git:git sample.git
vim /etc/passwd
把git:X:1001:1001:,,,:/home/git:/bin/bash
换为下面的
git:X:1001:1001:,,,:/home/git:/usr/bin/git-shell
git clone git@server:/srv/sample.git
下载地址
鼠标邮件,选择“TortoiseGit”->“Settings”,在“User Info”输入Name和Email
在命令行和tortoisegit中稍有不同:
In shell
a. win+r –> Git Bash –> ssh-keygen.exe
b. 一路回车,将生成“c:/Users/XXX/.ssh/id_rsa.pub”
c. (公钥)把id_rsa.pub重命名为id_rsa.XXX.pub, 例如id_rsa.jiangyikun.pub
d. (私钥)也已经生成到c:/Users/XXX/.ssh/id_rsa
了
e. 将id_rsa.XXX.pub发送给git管理员
In tortoisegit
a. 打开Puttygen,SSH-2 RSA,点击Generate
b. 使用鼠标生成随机key
c. (公钥)复制Key到id_rsa.XXX.pub
d. (私钥)Save private key为XXX.ppk,并将Load Putty Key设为XXX.ppk
e. 将id_rsa.XXX.pub发送给git管理员
以root权限登陆, 注意是“>>”追加
1 | cat id_rsa.XXX.pub >> /home/git/.ssh/authorized_keys |
在客户端测试:ssh git@XXX.XXX.XXX.XXX,不再输入密码即可。
这是一个用于测试的repo你可以拿他来做做测试
a. 克隆仓库
git clone git@xxx.xxx.xxx.xxx:/yikun/sample.git
或者使用tortoisegit,在某个文件夹,右键–>Git Clone…–> URL填写“git@xxx.xxx.xxx.xxx:/yikun/sample.git”–> 输入密码
b. 修改文件并提交
c. 点击push, ok
Enjoy it! :)
从现有repo新建server repo的时候(服务器)
1 | mkdir newrepo.git |
在现有的repo中(客户端)
1 | cd newrepo.git |
1 | 从服务器克隆 |
1 权限问题
1 | Counting objects: 179, done. |
可以试试:
1 | chown -R git * |
2 提交问题
1 | error: refusing to update checked out branch: refs/heads/master |
可以试试:
1 | git config 'receive.denyCurrentBranch' warn |
算法名称 | 复杂度 | 实现关键 |
---|---|---|
冒泡排序 | O(n^2) | (无序区,有序区)。从无序区通过交换找出最大元素放到有序区前端。 |
选择排序 | O(n^2) | (有序区,无序区)。在无序区里选择一个最小的元素跟在有序区的后面。 |
插入排序 | O(n^2) | (有序区,无序区)。把无序区的第一个元素插入到有序区的合适的位置。 |
希尔排序 | nlog^2(n) | 每一轮按照事先决定的间隔进行插入排序,间隔会依次缩小,最后一次一定要是1(插入)。 |
快速排序 | nlog(n) | (小数,枢纽元,大数)。 |
堆排序 | nlog(n) | |
桶排序 | O(n) | 将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。 |
不稳定的排序:
稳定性一个形象的比喻,本来有两个并列第三,一排序把原来并列的顺序给变了。
比如:选择排序、快速排序、堆排序、希尔排序;
参考链接
每次都把未排序的第一个作为起始点,然后逐渐冒泡上升,之后未排序区越来越少,最终排序完成
1 | // 冒泡排序 |
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
1 | // 选择排序 |
每次排序从未排序区取一个“牌”,然后往前插入(包括了两步:大的往后移,把牌放到合适位置)。
1 | // 插入排序 |
另外还有种思路,把数据后移的过程换成交换的过程
1 | // 插入排序,选中的牌冒泡向前插入 |
对插入排序再加一个步长的循环就是希尔排序了,例如1
[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ]
按照5步长排序,则相当于按列先进行排序(实际是通过下标实现的)1
2
3
413 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
排序后结果为1
2
3
410 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
多次循环后,只需要最终步长为1即可。
1 | // 希尔排序 |
每次迭代都选出一个基准,左边放小的,右边放大的,最终迭代完成。
1 | // 快速排序分区 |
首先,初始化连接池,
cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);if (cycle->connections == NULL) { return NGX_ERROR;}
我们可以看到cycle->connections被分配了一个足够的空间。
i = cycle->connection_n;next = NULL;do { i--; c[i].data = next; c[i].read = &cycle->read_events[i]; c[i].write = &cycle->write_events[i]; c[i].fd = (ngx_socket_t) -1; next = &c[i];} while (i);cycle->free_connections = next;cycle->free_connection_n = cycle->connection_n;
初始化完成后,连接池的样子就想一个前一个元素的一个数组。最后,free_connection指向第一个元素。我们可以看下
我们可以看下,初始化后连接池情况:
现在的结果就是,沿着free–>connection–>connection就连成了一串,然后get的时候直接把free_connection拿出来就可以了,然后free_connection指向原来的那个next。
ngx_connection_t *ngx_get_connection(ngx_socket_t s, ngx_log_t *log){ ngx_uint_t instance; ngx_event_t *rev, *wev; ngx_connection_t *c; /* disable warning: Win32 SOCKET is u_int while UNIX socket is int */ if (ngx_cycle->files && (ngx_uint_t) s >= ngx_cycle->files_n) { ngx_log_error(NGX_LOG_ALERT, log, 0, "the new socket has number %d, " "but only %ui files are available", s, ngx_cycle->files_n); return NULL; } /* ngx_mutex_lock */ //把free_connections给c,最后会返回c c = ngx_cycle->free_connections; //连接池不够了 if (c == NULL) { ngx_drain_connections(); c = ngx_cycle->free_connections; } if (c == NULL) { ngx_log_error(NGX_LOG_ALERT, log, 0, "%ui worker_connections are not enough", ngx_cycle->connection_n); /* ngx_mutex_unlock */ return NULL; } // free_connections指向下一个可用连接 ngx_cycle->free_connections = c->data; ngx_cycle->free_connection_n--; /* ngx_mutex_unlock */ if (ngx_cycle->files) { ngx_cycle->files[s] = c; } rev = c->read; wev = c->write; ngx_memzero(c, sizeof(ngx_connection_t)); c->read = rev; c->write = wev; c->fd = s; c->log = log; instance = rev->instance; ngx_memzero(rev, sizeof(ngx_event_t)); ngx_memzero(wev, sizeof(ngx_event_t)); rev->instance = !instance; wev->instance = !instance; rev->index = NGX_INVALID_INDEX; wev->index = NGX_INVALID_INDEX; rev->data = c; wev->data = c; wev->write = 1; return c;}
我们可以看下,get了三次后的连接池情况:
下面是free_connection的过程,连接释放后,重新加入到连接池的过程很像链表在头指针后插入节点的操作(其实就是),free之后,可能连接池的整体情况不像开始那样“整齐”,不过,我们把他当做链表来看,free_connection是头指针,通过c->data把指针一个一个串了起来,保证下次get的时候,get头节点的,free的时候,也是free头节点。
voidngx_free_connection(ngx_connection_t *c){ /* ngx_mutex_lock */ //free节点next指向 c->data = ngx_cycle->free_connections; ngx_cycle->free_connections = c; ngx_cycle->free_connection_n++; /* ngx_mutex_unlock */ if (ngx_cycle->files) { ngx_cycle->files[c->fd] = NULL; }}
我们可以看下,free了三次后的连接池情况:
以上就是连接池的基本操作。
]]>本文主要学习了nginx连接accept的步骤,以及nginx的负载均衡的方法。
处理新链接事件的回调函数是ngx_event_accept函数,当处理建立连接事件的时候就调用ngx_event_accept函数。该函数的具体步骤如下所示。
1.调用accpet方法。
s=accept(lc->fd, (struct sockaddr *)sa, &socklen)
2.设置负载均衡的阈值。
ngx_accept_disabled = ngx_cycle->connection_n/8 - ngx_cycle->free_connection_n;
3.从连接池获取连接。
c = ngx_get_connection(s, ev->log);
4.给连接分配内存空间。
5.设置套接字属性,设置非阻塞
ngx_nonblocking(s)
6.将连接加入事件循环。
ngx_add_conn(c)
#define ngx_add_conn ngx_event_actions.add_conn
7.调用监听对象的回调方法。
在建立连接前需要使用ngx_trylock_accept_mutex()去“抢锁”,抢到锁了之后,才有资格去accept连接。
代码比较简单,拿出来分析一下
ngx_int_tngx_trylock_accept_mutex(ngx_cycle_t *cycle){ //尝试锁,无论取到还是未取到,均立即返回,取到返回1,否则返回0。 if (ngx_shmtx_trylock(&ngx_accept_mutex)) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex locked"); if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { return NGX_OK; } //将所有读事件加入epoll if (ngx_enable_accept_events(cycle) == NGX_ERROR) { ngx_shmtx_unlock(&ngx_accept_mutex); return NGX_ERROR; } ngx_accept_events = 0; ngx_accept_mutex_held = 1; return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex lock failed: %ui", ngx_accept_mutex_held); //未取到锁,ngx_accept_mutex_held还为1,则删除所有监听的读事件 if (ngx_accept_mutex_held) { if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; } ngx_accept_mutex_held = 0; } return NGX_OK;}
总结一下就是说,拿到锁了,就监听事件,拿不到就不能监听。若ngx_accept_mutex_held为1,则拥有了把accept事件加入到自己的ngx_posted_accept_events的权利。
]]>在上篇文章中,介绍了事件模块的初始化工作,其中ngx_event_process_init会调用module->actions.init(对应epoll为ngx_epoll_init),该步调用的是epoll的初始化函数,之后,将rev的回调函数指向ngx_event_accept函数,这样便会accept新的链接。那么,本文将以epoll为例,来学习一下事件机制的主体。
我们的重点还是放在ngx_worker_process_cycle,处理事件的核心则是ngx_process_events_and_timers,其基本机制如下图所示,
在ngx_worker_process_cycle最开始的初始化中,epoll模块会调用epoll_creat初始化,之后便进入事件的循环中,然后epoll_wait有事件就加入到队列中,然后集中处理,如果拿到锁了就可以处理accept的事件,然后处理完accept后就解锁,之后再去处理普通读写事件。
事件的主循环主要分为三步
1.调用ngx_process_events。
#define ngx_process_events ngx_event_actions.process_events
而对于epoll来说就是调用ngx_epoll_process_events函数。
2.调用ngx_event_process_posted处理事件队列中的事件。
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
ngx_event_process_posted(cycle, &ngx_posted_events);
我们可以看见处理的网络事件主要牵扯到2个队列,一个是ngx_posted_accept_events,另一个是ngx_posted_events。其中,一个队列用于放accept的事件,另一个则是普通的读写事件;
ngx_event_process_posted会处理事件队列,其实就是调用每个事件的回调函数,然后再让这个事件出队。
例如,我们在开始的时候,已经把accept事件的回调函数指定为ngx_event_accept,那么当处理accept事件的时候便会调用这个函数。
我么可以看到,每个worker进程先抢锁,抢到锁的worker就获得所有监听的事件,这个worker来“接待”新的”accept”,当接待完ngx_posted_accept_events队列里面的连接后,就解锁。没拿到锁的,会更频繁的拿锁。最终实现了负载均衡。
3.处理定时器事件。
以上便是整个事件机制的实现。
]]>因为开始对nginx的模块机制不是很了解,所以开始看事件模块的这部分还是云里来,雾里去的,最后,算是理清了,我觉得应该抓几个核心的数据结构,无论是初始化,还是在事件循环中,都和这些数据结构息息相关。我们需要抓住这两个结构体,搞清楚他们在什么时候初始化,什么时候起作用,这样就可以理清大概的
nginx的事件机制最重要的牵扯到几个结构体,当然niginx的事件的初始化也围绕着这几个模块进行。
事件模块配置的初始化,主要是所有事件模块的配置创建与初始化的过程,与事件模块息息相关的是ngx_events_module结构体,定义了对events
的“兴趣”,以及初始化events的回调函数ngx_events_block。
首先,最重要的结构体是
ngx_module_t ngx_events_module = { NGX_MODULE_V1, &ngx_events_module_ctx, /* module context */ ngx_events_commands, /* module directives */ NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING};
该结构提为nginx的核心模块(NGX_CORE_MODULE),主要用于events配置的解析,那么初始化的时候围绕着这个结构体进行的当然也便是配置解析的相关工作了。
其次,这个模块还需要管理所有时间模块的配置,最后还有就是对各个事件模块的配置进行统一管理。
static ngx_command_t ngx_events_commands[] = { { ngx_string("events"), NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, ngx_events_block, 0, 0, NULL }, ngx_null_command};static ngx_core_module_t ngx_events_module_ctx = { ngx_string("events"), NULL, ngx_event_init_conf};
然后再看看ngx_events_module
的内容,主要牵扯到的变量有ngx_events_commands
、ngx_events_module_ctx
,ngx_events_commands
可以理解为告诉解析conf,解析到什么指令,调用什么函数,比如这里就是解析到events
指令调用ngx_events_block
函数。ngx_events_module_ctx
则是核心模块提供给各种模块实现时提供的接口,事件模块作为核心模块,也需要实现这个接口。
也就是说除了配置文件解析之外,这个模块没有做任何其他事情,让我们来关注一下ngx_events_block
。
之前的文章有介绍过nginx的初始化过程,在初始化的时候,会调用ngx_init_cycle函数,而该函数会调用ngx_conf_parse,其中ngx_conf_parse会完成对ngx_events_block
的调用,具体是怎么样调用的在以后再学习配置解析的时候再分析。现在只需要知道ngx_events_block调用时机是在ngx_conf_parse的时候就勾勒,重点看下ngx_events_block的实现,ngx_events_block执行过程如图所示,
下面将这五步详细介绍一下,
1.对事件模块进行标号。首先在ngx_modules[i]数组里面找到NGX_EVENT_MODULE
的变量,之后对每一个事件模块进行标号,也就是说ctx_index表示在相同类型模块中的标号。例如此处的就表示在所有事件模块中的标号。ngx_modules则是一个全局数组,位于obj/ngx_modules.c
目录,存储着所有的模块信息。
ngx_event_max_module = 0;for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } ngx_modules[i]->ctx_index = ngx_event_max_module++;}
2.为事件模块的配置分配空间。其次便是给事件模块配置的指针及配置所存储的指针数组分配空间。
//开辟红色部分空间ctx = ngx_pcalloc(cf->pool, sizeof(void *));if (ctx == NULL) { return NGX_CONF_ERROR;}//开辟蓝色部分空间*ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));if (*ctx == NULL) { return NGX_CONF_ERROR;}
在ngx_cycle_t中有一个conf的四级指针(conf_ctx)。它指向了一个指针数组A(存储着所有核心模块配置结构体指针),A中的指针又指向了另一个指针数组B(假设A中的这个指针是事件模块配置结构体的指针),那么B中就存着事件模块的配置。如图所示
3.对每个事件模块create_conf, 调用每个事件模块中的create_conf方法,m现在指向的就是每个模块的配置内容(即ctx, context,上下文)
for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } m = ngx_modules[i]->ctx; if (m->create_conf) { (*ctx)[ngx_modules[i]->ctx_index] = m->create_conf(cf->cycle); if ((*ctx)[ngx_modules[i]->ctx_index] == NULL) { return NGX_CONF_ERROR; } }}
4.解析events中的指令。调用ngx_conf_parse解析events块中的指令。
5.对每个事件模块init_conf。调用每个事件模块中的init方法
for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } m = ngx_modules[i]->ctx; if (m->init_conf) { rv = m->init_conf(cf->cycle, (*ctx)[ngx_modules[i]->ctx_index]); if (rv != NGX_CONF_OK) { return rv; } }}
完成每个conf的配置。至此,ngx_events_block
的工作就完了,总结一下就是负责配置文件中events block的解析。
其次,是ngx_event_core_module这个模块是一个事件类型(NGX_EVENT_MODULE)的模块
ngx_module_t ngx_event_core_module = { NGX_MODULE_V1, &ngx_event_core_module_ctx, /* module context */ ngx_event_core_commands, /* module directives */ NGX_EVENT_MODULE, /* module type */ NULL, /* init master */ ngx_event_module_init, /* init module */ ngx_event_process_init, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING};
先开始,把ngx_events_module
和ngx_event_core_module
搞混了,因为之前没有接触过nginx的模块,现在清楚了,当看到一个模块的时候,先看module type,ngx_event_core_module
的type是NGX_EVENT_MODULE。而比较特殊,他是NGX_EVENT_MODULE最核心的module,同样的,我们看看ngx_event_core_module的内容ngx_event_core_commands
存储着解析到”某些指令”回调”某些函数”。ngx_event_core_module_ctx
则存储着模块配置的创建与初始化函数。
static ngx_command_t ngx_event_core_commands[] = { { ngx_string("worker_connections"), NGX_EVENT_CONF|NGX_CONF_TAKE1, ngx_event_connections, 0, 0, NULL }, { ngx_string("connections"), NGX_EVENT_CONF|NGX_CONF_TAKE1, ngx_event_connections, 0, 0, NULL }, { ngx_string("use"), NGX_EVENT_CONF|NGX_CONF_TAKE1, ngx_event_use, 0, 0, NULL }, { ngx_string("multi_accept"), NGX_EVENT_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, 0, offsetof(ngx_event_conf_t, multi_accept), NULL }, { ngx_string("accept_mutex"), NGX_EVENT_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, 0, offsetof(ngx_event_conf_t, accept_mutex), NULL }, { ngx_string("accept_mutex_delay"), NGX_EVENT_CONF|NGX_CONF_TAKE1, ngx_conf_set_msec_slot, 0, offsetof(ngx_event_conf_t, accept_mutex_delay), NULL }, { ngx_string("debug_connection"), NGX_EVENT_CONF|NGX_CONF_TAKE1, ngx_event_debug_connection, 0, 0, NULL }, ngx_null_command};ngx_event_module_t ngx_event_core_module_ctx = { &event_core_name, ngx_event_core_create_conf, /* create configuration */ ngx_event_core_init_conf, /* init configuration */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }};
另外,ngx_event_core_module还定义了2个函数。我们来看看这两个函数的调用情况
ngx_event_module_init, /* init module */ngx_event_process_init, /* init process */
这两个函数的调用的时机,如下图所示:
ngx_event_module_init是在ngx_init_cycle被调用的,主要就是初始化模块的一些变量。
ngx_event_process_init则是在worker进程开始时被调用,之后便进入事件循环中,主要包括了负载均衡锁的初始化、定时器的初始化、连接池的初始化,以及在最后遍历所有modules来调用modules数组中的ngx_add_event函数,将事件添加到监听队列中。可以看到
#define ngx_process_changes ngx_event_actions.process_changes#define ngx_process_events ngx_event_actions.process_events#define ngx_done_events ngx_event_actions.done#define ngx_add_event ngx_event_actions.add#define ngx_del_event ngx_event_actions.del#define ngx_add_conn ngx_event_actions.add_conn#define ngx_del_conn ngx_event_actions.del_conn
这就是nginx事件模块的精华所在,通过这样的方式,就可以使得ngx_event_actions不同,采用不同的复用机制。可以参照下图,来理解ngx_event_core_module。
上面可以看到红色的部分是ngx_event_core_module有用的部分,需要强调的是这个事件模型只是用来初始化类似epoll的模块的,而自己是不做一些类似epoll事件循环的具体事件的。
至此,事件初始化就结束了,可以看到上面都是nginx通用的,不牵扯到具体的复用机制,后面会根据epoll来具体学习一下nginx事件循环。
]]>###我的vim插件概览###
pathogen.vim
用于插件管理,把插件放到bundle目录就可以了。
Trinity.vim
用于集中管理taglist、nerdtree、srcexpl三个插件,按 F8
就可以快速打开这三个插件了。
taglist.vim
用于生成、展示函数列表
nerdtree.vim
用于生成、展示目录和文件
srcexpl.vim
用于函数的展示,当移动到函数上的之后,就会在srcexpl的窗口里面显示函数定义的。
CSApprox.vim
这是一个vim配色的插件,我比较喜欢desert这个配色。
OmniCppComplete.vim
用于代码补全工作,能够加快效果
a.vim
用于快速切换.c和.h
ctrlp.vim
用于文件的模糊搜索,能够加快打开文件的速度效果
powerline.vim
一个优雅的状态栏插件
vimgrep
自带插件,用于搜索tags中匹配字符,可以查看函数调用情况效果
###如何使用的我的配置###
最简单的方法就是clone所有的配置到你的.vim
目录之中,首先交代下我的vim配置方法,与vim相关的配置主要是.vim
里的插件,一般目录位于~/.vim
,还有vim的配置文件~/.vimrc
,为了让配置vim更简单些,利用了ln -s ~/.vim/.vimrc ~/.vimrc
命令,这样,就相当于~/.vimrc
是一个指向~/.vim/.vimrc
的快捷方式。因此使用我的配置,也很简单。
第一步,备份。
# mv ~/.vim backup# mv ~/.vimrc backup
第二部,复制配置。
# git clone git@github.com:Yikun/vim-config.git ~/.vim
第三部,创建.vimrc链接
# ln -s ~/.vim/.vimrc ~/.vimrc
这样就ok了。
]]>由于nginx使用的是多进程的模型,因此,进程间的通信或者同步很重要,为什么要进行进程同步呢?我们知道,nginx有master和worker进程,在上篇文章已经分析过了master具体是怎样创建worker进程的。不过,在创建worker进程的时候,是需要对进程同步的。举个具体的例子,我们假设服务器共有4个worker进程,我们知道nginx有一个全局变量,是ngx_processes数组,他存储着所有进程的信息,在worker1创建的时候,worker2,worker3,worker4进程是没有创建的,因此,这个时候就牵扯到同步,最合理的方式是,在master创建一个进程的时候,就应该通知所有子进程有新的进程被fork了,以及这个进程的基本信息。
这个好比一个集体(由很多processes组成),当有新的成员加入这个集体的时候,老大应该告诉大伙,有新成员进来了,他的基本信息是balabala。因此,也就引出了本文所要总结的内容,即nginx的进程通信机制。
我们先回顾一下worker进程的创建过程,ngx_master_process_cycle -> ngx_start_worker_processes,在 ngx_start_worker_processes
函数中,有下面的代码
static voidngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type){ ngx_int_t i; ngx_channel_t ch; ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes"); ch.command = NGX_CMD_OPEN_CHANNEL; for (i = 0; i < n; i++) { ngx_spawn_process(cycle, ngx_worker_process_cycle, (void *) (intptr_t) i, "worker process", type); ch.pid = ngx_processes[ngx_process_slot].pid; ch.slot = ngx_process_slot; ch.fd = ngx_processes[ngx_process_slot].channel[0]; ngx_pass_open_channel(cycle, &ch); }}
注意观察下, ngx_channel_t
结构体的定义如下:
typedef struct { ngx_uint_t command; ngx_pid_t pid; ngx_int_t slot; ngx_fd_t fd;} ngx_channel_t;
没错,这个就是master与worker进程通信的最重要的结构,短小精汗。
该结构封装了四个变量,分别是指令(master要worker干啥),pid(worker的进程id),slot(worker进程在ngx_processes的索引),文件描述符。我们思考一下概述中的那个问题,怎么将master后创建的进程通知前面已创建的进程。 ngx_pass_open_channel(cycle, &ch);
注意一下这个函数,没错就是它了,通过它对每个进程进行通知。
那么具体又是怎么实现通知的呢?我们看到在 ngx_channel_t
中有一个 ngx_fd_t fd;
这个文件描述便存储着通信的“接口”,从之前的代码我们看出来, ch.fd = ngx_processes[ngx_process_slot].channel[0];
这个channel[0]是真正传输的接口。那么他是什么呢?简单的说,就是master写给每个process的channel[0]一些信息(ngx_channel_t的实际内容),worker就能在自己的channel[1]中,读取到这些信息。
nginx使用的是 socketpair
方法关联套接字,我们看看socketpair的原型:
int socketpair(int d, int type, int protocol, int sv[2]);
我们关注一下第四个参数,当这个socketpair函数执行成功后,就会生成一个socket对在数组中,sv[2]中的socket是关联起来的,什么意思呢?就是说向sv[0]写数据,在sv[1]就能读到相应的数据;相反的,在sv[1]写数据,在sv[0]也可以读到相应的数据。在master进程fork worker进程的时候,也把这个套接字传给了worker,也就是说在master向worker的sv[0]写数据,那么worker便可以在自己的sv[1]中读到数据。
nginx的具体的实现方式如上图所示:
channel[0]和channel[0]为一对socketpair。
1. 向channel[0]写数据时,可从channel[1]读数据;2. 向channel[1]写数据时,可从channel[0]读数据。
而nginx,只利用了第一条,即master向channel[0]写数据时,worker可从channel[1]读数据
socketpair也用来进行父子进程的通信,子进程会继承父进程的资源。
我们具体的来看下nginx写入数据的过程,
ngx_write_channel(ngx_processes[i].channel[0], ch, sizeof(ngx_channel_t), cycle->log);
和上节介绍的一样,我们看到master对每一个ngx_processes[i].channel[0]写入数据。并且写入的数据就是 ngx_channel_t
变量。
好了,既然master向worker写数据的接口有了,那么woker又怎么对master写入的数据进行读取和处理呢?
我们目光移到worker进程上面,ngx_worker_process_cycle函数,在初始化时,调用了 ngx_worker_process_init
函数,这个初始化函数又调用了
ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,ngx_channel_handler)
这个就利用了nginx强大的事件机制,这个函数大概的功能就是,如果worker channel[1]有可读的数据,便会调用 ngx_channel_handler
进行处理。
switch (ch.command) {case NGX_CMD_QUIT: ngx_quit = 1; break;case NGX_CMD_TERMINATE: ngx_terminate = 1; break;case NGX_CMD_REOPEN: ngx_reopen = 1; break;case NGX_CMD_OPEN_CHANNEL: ... ngx_processes[ch.slot].pid = ch.pid; ngx_processes[ch.slot].channel[0] = ch.fd; break;case NGX_CMD_CLOSE_CHANNEL: ... if (close(ngx_processes[ch.slot].channel[0]) == -1) { ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno, "close() channel failed"); } ngx_processes[ch.slot].channel[0] = -1; break;case NGX_CMD_PIPE_BROKEN: ngx_pipe_broken_action(ev->log, ch.pid, 0); break;}
由于nginx目前的读写数据只是单向的即mater–>worker,因此,这些指令的解析,都是需要让worker做一些事儿。我们可以关注一下 NGX_CMD_OPEN_CHANNEL
这个分支。在ngx_start_worker_processes函数中,master就向worker写入了 NGX_CMD_OPEN_CHANNEL
指令。
那么worker进程,便根据这个 ngx_channel_t ch
信息,更新processes数组。这样便完成了进程的同步。
我们发现,ngx_channel_handler中共有6个指令类型,分别是NGX_CMD_QUIT、NGX_CMD_TERMINATE、NGX_CMD_REOPEN、NGX_CMD_OPEN_CHANNEL、NGX_CMD_CLOSE_CHANNEL、NGX_CMD_PIPE_BROKEN。下面我们分析下,和channel相关的命令。
NGX_CMD_OPEN_CHANNEL
之前,我们已经分析了NGX_CMD_OPEN_CHANNEL信号的解析大致过程,现在仔细观察一下,我先搜索了一下使用NGX_CMD_OPEN_CHANNEL
命令的地方,对ch.command
赋值的地方有四处。第一处是用于worker进程的,第二、三处是和cache manager进程有关的,暂不关注,第四处,是ngx_reap_children
主要是用于nginx重启后,重新开启channel的。
我们只分析第一处,master进程的函数ngx_start_worker_processes
在开启worker进程的时候,把命令设置为NGX_CMD_OPEN_CHANNEL
,并且通过ngx_write_channel
把指令给相应的进程,这样当worker进程解析这个消息时,便根据新进程的slot把新进程的信息(新进程的pid、新进程的channel[0])保存起来。
上图已经表明了NGX_CMD_OPEN_CHANNEL
的传递与生效过程。
分为2个部分
总结一下,这个命令就是告诉worker,有新的进程来,他OPEN_CHANNEL了,你得存起来,然后worker就存这个新进程的信息了,当然这个信息是存在processes数组里了。
NGX_CMD_CLOSE_CHANNEL
当然,与打开对应的就是关闭channel指令了,与这个命令相关的赋值只有一处,就是ngx_reap_children
,当然就是master向每个进程更新信息,如果发现某个进程exited了,就告诉大家,可以把它的channel关闭了,即把这个channel的flag置为-1。而关闭的时候,close(ngx_processes[ch.slot].channel[0])
关闭了channel[0],先开始有疑问了,为什么只关0呢?1怎么办?原来1其实在work刚开始的时候就关闭了,即最开始就已经“关闭了除了自己外的channel[1],然后再关闭自己的channel[0]。
总结一下,这个命令就是告诉work,你要关闭这个CHANNEL了,原因从目前的nginx代码来看,只有一个,就是需要重启。关闭已经exited的进程的channel。
不过,有些疑问,
1.目前来看只有master向worker的消息,不存在worker向master,或者worker向worker写了,那么为什么不关闭其他worker的channel[0]呢?我觉得可能是不是和cache load进程有关,后面再思考一下。
2.为什么master中要保留所有子进程channel[1]?可以在fork完子进程,就关闭,为什么不关闭呢?
]]>最近,开始学习nginx的代码,大致根据阿里数据平台的一些文章,加上Tengine 2.0的代码来看的。这次看代码主要是了解一下nginx的基本框架和主要工作流程。
如下图所示,为总的启动流程分析,后面是我对每个部分的总结和分析
nginx是由C语言写成的,因此,从main函数开始开启我们的“旅程”,传入参数为argc,还有argv,最开始的任务当然就是解析它们了,以获得用户启动的参数,调用ngx_get_options解析参数,一般情况,Linux的解析命令参数都会调用getopt之类的系统函数,而nginx却没有,应该是考虑到了跨平台性。解析命令参数的代码比较简单,大致的工作就是标记flag,类似ngx_show_version,ngx_show_modules的全局参数可以记录命令参数。
而后,根据这些flag来做一些事情,例如使用nginx -h,会将ngx_show_version,ngx_show_help置为有效(1),然后后面回到main后,就是做一些对应的输出。
包括了time、regex、log、ssl等初始化,而后进行一个很重要的结构的初始化ngx_cycle。
struct ngx_cycle_s { void ****conf_ctx; //配置上下文数组(含所有模块) ngx_pool_t *pool; //内存池 ngx_log_t *log; //日志 ngx_log_t new_log; ngx_connection_t **files; //连接文件 ngx_connection_t *free_connections; //空闲连接 ngx_uint_t free_connection_n; //空闲连接个数 ngx_queue_t reusable_connections_queue; //再利用连接队列 ngx_array_t listening; //监听数组 ngx_array_t paths; //路径数组 ngx_list_t open_files; //打开文件链表 ngx_list_t shared_memory; //共享内存链表 ngx_uint_t connection_n; //连接个数 ngx_uint_t files_n; //打开文件个数 ngx_connection_t *connections; //连接 ngx_event_t *read_events; //读事件 ngx_event_t *write_events; //写事件 ngx_cycle_t *old_cycle; //old cycle指针 ngx_str_t conf_file; //配置文件 ngx_str_t conf_param; //配置参数 ngx_str_t conf_prefix; //配置前缀 ngx_str_t prefix; //前缀 ngx_str_t lock_file; //锁文件 ngx_str_t hostname; //主机名};
ngx_init_cycle的过程的详细情况可以参考Nginx启动初始化过程(二)。因为现在功力不是很深,等以后对nginx有透彻了解后,再仔细分析。这里第一次出现了内存池的操作,后面重点分析一下内存池的实现。
ngx_init_signals会进行信号处理的初始化,signals是一个结构体数组,存储着各种信号的结构体,在初始化的过程中,会利用sigaction函数对每个信号进行设置,如下所示,主要是对signo和handler回调函数进行设置。初始化成功以后,当信号产生以后,便可以调用信号处理函数了,因此可以利用ngx_signal_handler进行信号处理了。
ngx_int_tngx_init_signals(ngx_log_t *log){ ngx_signal_t *sig; struct sigaction sa; for (sig = signals; sig->signo != 0; sig++) { ngx_memzero(&sa, sizeof(struct sigaction)); sa.sa_handler = sig->handler; sigemptyset(&sa.sa_mask); if (sigaction(sig->signo, &sa, NULL) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "sigaction(%s) failed", sig->signame); return NGX_ERROR; } } return NGX_OK;}
在启动过程中,会调用ngx_daemon(cycle->log),这个函数实现的很经典。
ngx_int_tngx_daemon(ngx_log_t *log){ int fd; //父进程fork switch (fork()) { //fork执行完后,master的 case -1: //fork出错了 ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed"); return NGX_ERROR; case 0: //master daemon(子进程)什么也不做 break; default: //master前台进程(父进程)退出,以给控制终端一个“假象”,这个程序执行完了 exit(0); } /* 执行到这,说明最开始的“前台”进程已经退出了,这时得刷新下ngx_pid,以便后面ngx_create_pidfile用(用来优雅的重启) 当然,有人问为什么main最开始就记录了,ngx_pid呢?那是因为nginx不一定会daemon形式启动,这样开始的进程就是master 然而在这里,nginx将原来的前台master exit掉,然后master fork出来的,所以这里的ngx_pid就是就是daemon master的了。 */ ngx_pid = ngx_getpid(); /* 作为daemon只fork还是不够的,需要第二步,setsid,他的作用是让daemon成为真正的daemon 1.会话组的老大; 2.进程组的老大; 3.不受任何控制终端控制 */ if (setsid() == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed"); return NGX_ERROR; } //umask(0)是为了让读写权限保持原来的状态 umask(0); //后面几句就是把STD的输入/输出/错误都输出到/dev/null,也就是什么也不输出 fd = open("/dev/null", O_RDWR); if (fd == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "open(\"/dev/null\") failed"); return NGX_ERROR; } if (dup2(fd, STDIN_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed"); return NGX_ERROR; } if (dup2(fd, STDOUT_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed"); return NGX_ERROR; }#if 0 if (dup2(fd, STDERR_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed"); return NGX_ERROR; }#endif if (fd > STDERR_FILENO) { if (close(fd) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed"); return NGX_ERROR; } } return NGX_OK;}
守护进程指的是后台运行不与任何控制终端相联的进程,许多网络服务器都作为守护进程运行。那么,什么样的进程才是守护进程呢?2种方法:
1. 这个进程是“富二代“,由内核无终端启动;2. 是靠自己后天努力,这个后天努力需要借助setid的帮助,新建一个会话,这样这个进程就成老大了,而且不受任何终端控制。
注释已经写的很详细了,总结一下就是以下几步。
1. fork一个daemon进程,退出前台进程。2. setsid 让daemon彻底脱离控制终端(如果没用这步的话,就会造成终端一退出,进程也就退了)3. umask(0)4. 不让他输入输出5. 改变目录,避免父进程工作目录的影响(nginx没做)6. 关闭没用的fd
这个是APUE中提到的6步。当然,也有人建议进行第二次fork,二次fork的原因是不让进程重新被终端控制。是这样的,如果一个进程是一个不属于任何一个终端的会话组的首进程,当这个进程打开终端的时候,系统就会为他分配一个终端,这样就惨了,它又要受终端控制了(一个会话组的首进程如果不属于任何终端,则该进程打开终端时会被分配终端,一个会话如果属于某个终端,就会有一个前台进程组),也就做不成守护进程了。不过要是二次fork的话,daemon A fork 出来 daemon B,这个daemon B不是会话首进程,就不会被分配到终端控制了。但是nginx没做,我觉得可能是因为nginx不会作类似操作吧。
注:不过2次fork,要记得Sighnal(SIG_HUP, SIG_IGN),否则daemon A作为首进程退出的时候,会告诉所有的小弟(包括B了)。
具体的守护进程参考UNP和APUE中的资料。
在完成main中的初始化后,我们的“初始化”旅程到了结尾,热身结束,开始重点。到调用这个函数的时候,nginx还是只有master进程的,作为master进程的开始工作,最终要的就是启动“work”进程。其实,很多软件都有master,work的概念,诸如Hadoop的jobtracker、tasktracker。master处理和用户的交互,然后work专心的去做业务,这样的话,master可以想象为一个管理者,work则是真正的工人。
屏蔽一下干扰
sigemptyset(&set);sigaddset(&set, SIGCHLD);sigaddset(&set, SIGALRM);sigaddset(&set, SIGIO);sigaddset(&set, SIGINT);sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "sigprocmask() failed");}sigemptyset(&set);
最开始的工作就是做一些信号处理的工作,首先将系统信号,nginx自定义的信号加入’sigset_t set;’信号集中,然后调用sigprocmask进行信号的屏蔽,函数为 ‘sigprocmask(SIG_BLOCK, &set, NULL)’ ,第一个参数为SIG_BLOCK意思就是按照set屏蔽信号,也就是说把之前通过 ‘sigaddset’ 的10个信号都屏蔽掉了,以防止在fork Work的过程中发生的意外。
master进程在屏蔽完信号干扰后,便调用了ngx_start_worker_processes来启动worker进程,这个函数的核心就是一个for循环,调用ccf->worker_processes次ngx_spawn_process函数,fork了ccf->worker_processes个worker。
ngx_spawn_process则是真正fork worker的函数。
pid = fork();switch (pid) {case -1: ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "fork() failed while spawning \"%s\"", name); ngx_close_channel(ngx_processes[s].channel, cycle->log); return NGX_INVALID_PID;case 0: ngx_pid = ngx_getpid(); proc(cycle, data); break;default: break;}
又是熟悉的fork了,能进入case 0的就是worker进程。而master进程则继续ngx_master_process_cycle,在worker都被master fork出来之后,master就要正常开始他的工作了
for ( ;; ) {// ... sigsuspend(&set);// ...}
这个就是master的工作框架,简单吧?就是休眠,等信号,做事儿,再休眠,等信号,做事儿。sigsuspend(&set);就是让进程休眠,直到有信号的时候,去处理。
在main开始初始化的时候,就通过’ngx_init_signals’对每个信号的回调函数进行初始化,也就是说,每次信号来了都会调用 ‘ngx_signal_handler’ 去设全局的flag,如果有信号了,master的 ‘ngx_master_process_cycle’ 就会对这些全局flag进行对应的处理。
最后,总结一下master的工作,就是先把信号都屏蔽了,然后去fork worker进程,fork完work以后,master就进入信号处理的循环了,利用sigsuspend等信号,等到信号就处理,处理完了再sigsusoend,如此循环,完成伟大的幕后工作。
worker开始工作的真正时候,应该是在master调用ngx_spawn_process之后的,master传入的proc参数就是ngx_worker_process_cycle函数指针,再回到刚才master中那个fork的过程,case 0的时候调用了proc(cycle, data);也就是相当与调用了ngx_worker_process_cycle,这样worker的工作也马不停蹄的开始了。
首先,惯例,进行初始化,ngx_worker_process_init,这里面就包括了自身的初始化,还有去除一下从master过来的没用的东西,比如sigprocmask一下,把之前master的屏蔽掉信号都开启了。这样,才能对master的信号进行处理,以便完成master和work的进程间的通信。
然后就开始真正的工作了,也是一个大循环。
for ( ;; ) {// ... ngx_process_events_and_timers(cycle)// ...}
到此worker的框架也就这样了,然后for循环的底部会有一些对master发来的信号的处理。
至此,master和worker的初始化工作以及基本的框架算是完了,经过上面的学习以后,发现对nginx的整个流程有了一个大概的认识。学习初始化的过程中,我学到了daemon,多进程,信号处理等基本知识。
]]>1.Start your java application, set the breakpoint in java before call jni function
java -Xdebug -Xrunjdwp:transport=dt_socket,address=6666,server=y,suspend=y com.kero.test.HelloJNI
2.Look up the pid using top, ps, …
ps -ef |grep com.kero.test.HelloJNI
3.Start gdb with this pid
gdb -p pidnum
or
gdb -p $(ps -ef |grep com.kero.test.HelloJNI |grep -v ‘grep’|awk ‘{print $2}’)
4.Attach your program code
dir XXX/HelloJNI/
5.Debug as usual using gdb
b function_name_XXX
6.Continue in java
Now, you will it will stop in c code where you set breakpoint.
]]>说实话,嵌入式比赛应该是我准备的最充分的也是最重视的比赛,首先这个比赛也算是大学阶段的最后一次了,其次比赛的结果也会是间接影响我未来的走向:工作还是上研。年初,记得很清楚,初十,校队的12人就已经全部到老校区开始了培训。我们住在留学生的宿舍,那宿舍旧的我们无语凝咽了,除了暖气,没有一样符合这个时代的特征,就是那种五六十年代的楼层。实验室的条件还算好,本来在主楼,最后到新科技楼付老师的实验室去了。每天的内容就是:想题目。想完题目,PPT答辩,老师点评,1-2天一个循环。那时候真是感觉自己的创意都被挖干净了,而且我们大家总是进入了怪圈,3D投影,虚拟现实,感觉还是限制在往届的思路中。时间过的很快,马上就开学了,题目基本还是没怎么完全定下。
到了开学,3月份去了一次上海,把平台拿回来的之后,基本上大家都住在新校区的实验室了,F520,估计一辈子也不会忘记这个实验室吧。每天的生活就是实验室和餐厅的两点一线,为什么没有宿舍?因为我们已经都扎根实验室了,在实验室买了气垫床,因为有空调的原因还是在实验室过的比较惬意,至少在夏天酷暑之时,免去了大汗淋漓的痛苦。晚上2点多睡,早上不到8点就起,其实我们也不想起那么早,因为实验室有女生啊,女生都是不熬夜的但是早起的生活规律的学霸呀,记得有次比较夸张,看到太阳从工训中心快升起來了,赶紧睡觉了,可过了一会,刚升起来了,女生来了,我们就都起来了。
那时候特别好玩,我和君朋,雨舟起床的时候都是不会互相叫的,我是觉得晚上睡得挺晚的,大家都听辛苦的,自己起来就多干点活吧。另外,因为夏天大家也就光穿一件就睡了,往往早上起来女生进来的时候,那就是个春光乍泄的景色。有时候也互相拍个照片留个“纪念”。所以,这样一来,大家都把闹铃弄得早于女生进实验室。可是君朋大神的闹钟总是叫不起他,一般情况下就是,他的闹钟把我们都叫起来了,然后我们把他闹钟关了。然后君朋起来的时候,总是睡意朦胧的问我,几点起的?我说,女生来之前……然后君朋看看我,不说话了。哈哈哈哈。
晚上睡的最晚的是子兼大神,他是信安那边的,比我们低一级,基本上就是不睡,然后在我们睡觉的时候,总能听见他在骂着什么,基本上也就一个字“艹”,估摸着是bug卡住了吧。我和君朋就叫他“艹艹哥”了。嵌入式的几组里面,进展最快的应该是谢老师的那一组,谢老师给他们安排的比较紧,一步一步,到最后也就比较规律了,不像我们,像没头苍蝇一样撞来去,云阳不停地调飞控,我们背后就是四轴飞行器螺旋桨不停地旋转,只有我们一组算是基本上纯软件的项目,所以,当时是真心比较心虚,毕竟别的组的实物都摆出来了,我们还是干巴巴的代码,图形都没有做好最后渲染,确实有点急了,君朋的FPGA的弄的也特别紊乱,还好有之前君朋做过FPGA的东西,也算是慢慢地有些进展了。
比赛中夹杂着高密度的期末考试,合起来大概有快10门课,那时候也顾不上那么多了,毕竟还是把比赛放在第一位了,基本上就是上午考的复习一晚上,下午考的复习一上午。不过还算是比较理想,至少还都在80左右。对于我这种在成绩上没什么打要求的来说,已经不错了。这时候就大概5月份了,5月的时候又有一件比较重要的事情就是国家大学生创新性实验计划的结题。
因为之前就想到过会遇到这个情况,所以,在年初的时候,就和DC说好,把服务器做好,然后,我这边Android端也基本上没什么问题了,大概3月多就基本全部搞定了。之前,中期答辩的时候得的是优,所以也没什么大的压力,所以感觉我作为负责人还是安排的比较合理的,嘿嘿。到后面就是写写结题报告,然后就答辩了。答辩的时候很幸运的进了复赛,最后的成绩还不错,第六名,如果提前一名,就可以有一个保研资格,谢胖也就不用现在苦逼的去考研了。话说回来,也快到考研了, 倒计时也是个位数,希望付出都有收获吧。
7月中旬的时候,就去上海了,票不好买,学校坑爹的定的硬座,付老师和任老师和我们一起,累死了快到宾馆,到了宾馆也没怎么休息,中午吃了饭,就赶紧收拾收拾调试平台了,然后雨舟去抽签了,付老师带队去的,付老师走前说了一句话:要是抽到明天,也就别睡了,直接通宵准备答辩吧。突然,我心中就有种不详的预感,雨舟大神总是一个充满着惊喜的人物。
果不起然,雨舟大神没让我们失望,第二天早上第三队!跪了。然后就开始准备,到晚上11点多的时候,在付老师的房间,来了一次四个队伍的整体模拟答辩,然后就1点多了,我们回到自己的房间,按着老师的意思修改了下,就准备睡了,实在困的不行了,一个电话想起,老师说凌晨2点半的时候再过去答辩一次。我们回到自己的房间,我检查了下,软件和驱动部分没问题,然后君朋就开始调手势的指环了,我就在改ppt。
突然,君朋告诉我FPGA坏了,我靠,我当时那是一身冷汗啊,跪的心都有了,脑海里呈现的就是3、4个月的辛苦白费了,不过我还是比较镇定的说,再检查一下,看看有啥问题,其实心中早已经是翻江倒海了。君朋把所有线都拔了,我说你重新插一下,看看是不是短路了。之后,把程序重新烧了一遍就好了。我那个心呀,终于放下了,也基本不困了,真心是吓清醒了。
然后最后一次答辩老师就比较细致了,包括应该怎么修改怎么注意细节。最后主要是一些鼓励我们的话,然后就四点快五点了,我们的ppt还算比较好没什么大的修改,付老师那组的ppt听说是谢老师跟他们改的通宵。我回去,君朋先睡半小时,说我改好ppt就叫他,我改好ppt大概就5点多了,看着君朋睡的香,我都不忍心叫他了,哈哈。没办法,因为还有几个小时就要去答辩了,所以还是叫他起来了。
我就睡觉了,睡了一个小时,就起来了,君朋还在调试程序,收拾了下东西,洗了个澡,就准备出发了。出发的时候君朋告诉我,2对指环(有一对备份),其中有一个短路了。我突然就想到了不知道哪位前辈对与程序说的一句名言,“只要你觉得会出bug的地方,就一定会出bug”。还别说陕西地方邪,我觉得上海也挺邪的。到了交大的电信大楼,真心出错了,失效了,复位也没用。眼看着就要开始答辩了,君朋居然把430的程序又重新下了一遍,就快开始的时候,君朋说,ok了,我和雨舟算是放心了,雨舟在填功能测试表,我去弄ppt了。之后的答辩还算比较不错,演示效果也算是正常发挥了。
回到宾馆,繁忙了半年的身心,终于放松了。在宾馆,我跟君朋说,希望回到西安,就能开始骄奢淫逸的生活了。最后的结果还算不错了,1个国一,3个国二,我们是国二。这样,我靠着竞赛的成绩,也就被保研了。
之后,我们在上海转了转,后面,还去杭州转了一圈,主要就去了西湖,时间安排的太紧,加上天气比较热,所以,也没觉得有什么特别美好的地方。惬意的是在西湖旁的回廊中的微风,西湖游行的客船,还有三潭映月的水畔。
别了上海,又回到西安,大学的最后一年,就开始了。
比较幸运吧,10月初的时候,在大街网上,收到了一个面试邀请,然后经过邮件面试,电话面试,两轮现场面试,终于拿到了实习的offer。这算是我第一份offer吧,上学期自己也确实准备过一些实习的面试,不过因为准备的不充分,深受打击,这次抱着试一试的态度,狠下心来,好好准备,意外的收获。在IBM 2个月了收获还算比较多,主要方面就是修改一些bug,还有一些性能测试。主要用shell和java比较多。
之前,一直在学校,确实没有工作方面的经验,不过和自己的想象还比较相似。不过,工作比我想象的稍微忙碌一些,我常常说的是,上班竞赛节奏,下班保研节奏。实习会持续到明年的6月左右,时间还长。后面再认真总结。
此页提供了 Markdown 的简单概念, 语法说明 页提供了完整详细的文档,说明了每项功能。但是 Markdown 其实很简单就可以上手,此页文档提供了一些范例,并且每个范例都会提供输出的 HTML 结果。
其实直接试试看也是一个很不错的方法, Dingus 是一个网页应用程序,你可以把自已编写的 Markdown 文档转成 XHTML。
一个段落是由一个以上的连接的行句组成,而一个以上的空行则会划分出不同的段落(空行的定义是显示上看起来像是空行,就被视为空行,例如有一行只有空白和 tab,那该行也会被视为空行),一般的段落不需要用空白或换行缩进。
Markdown 支持两种标题的语法,Setext 和 atx 形式。Setext 形式是用底线的形式,利用 =
(最高阶标题)和 -
(第二阶标题),Atx 形式在行首插入 1 到 6 个 #
,对应到标题 1 到 6 阶。
区块引用则使用 email 形式的 ‘>
‘ 角括号。
Markdown 语法:
A First Level Header====================A Second Level Header---------------------Now is the time for all good men to come tothe aid of their country. This is just aregular paragraph.The quick brown fox jumped over the lazydog's back.### Header 3> This is a blockquote.> > This is the second paragraph in the blockquote.>> ## This is an H2 in a blockquote
输出 HTML 为:
<h1>A First Level Header</h1><h2>A Second Level Header</h2><p>Now is the time for all good men to come tothe aid of their country. This is just aregular paragraph.</p><p>The quick brown fox jumped over the lazydog's back.</p><h3>Header 3</h3><blockquote><p>This is a blockquote.</p><p>This is the second paragraph in the blockquote.</p><h2>This is an H2 in a blockquote</h2></blockquote>
Markdown 使用星号和底线来标记需要强调的区段。
Markdown 语法:
Some of these words *are emphasized*.Some of these words _are emphasized also_.Use two asterisks for **strong emphasis**.Or, if you prefer, __use two underscores instead__.
输出 HTML 为:
<p>Some of these words <em>are emphasized</em>.Some of these words <em>are emphasized also</em>.</p><p>Use two asterisks for <strong>strong emphasis</strong>.Or, if you prefer, <strong>use two underscores instead</strong>.</p>
无序列表使用星号、加号和减号来做为列表的项目标记,这些符号是都可以使用的,使用星号:
* Candy.* Gum.* Booze.
加号:
+ Candy.+ Gum.+ Booze.
和减号
- Candy.- Gum.- Booze.
都会输出 HTML 为:
<ul><li>Candy.</li><li>Gum.</li><li>Booze.</li></ul>
有序的列表则是使用一般的数字接着一个英文句点作为项目标记:
1. Red2. Green3. Blue
输出 HTML 为:
<ol><li>Red</li><li>Green</li><li>Blue</li></ol>
如果你在项目之间插入空行,那项目的内容会用 <p>
包起来,你也可以在一个项目内放上多个段落,只要在它前面缩排 4 个空白或 1 个 tab 。
* A list item.With multiple paragraphs.* Another item in the list.
输出 HTML 为:
<ul><li><p>A list item.</p><p>With multiple paragraphs.</p></li><li><p>Another item in the list.</p></li></ul>
Markdown 支援两种形式的链接语法: 行内 和 参考 两种形式,两种都是使用角括号来把文字转成连结。
行内形式是直接在后面用括号直接接上链接:
This is an [example link](http://example.com/).
输出 HTML 为:
<p>This is an <a href="http://example.com/">example link</a>.</p>
你也可以选择性的加上 title 属性:
This is an [example link](http://example.com/ "With a Title").
输出 HTML 为:
<p>This is an <a href="http://example.com/" title="With a Title">example link</a>.</p>
参考形式的链接让你可以为链接定一个名称,之后你可以在文件的其他地方定义该链接的内容:
I get 10 times more traffic from [Google][1] than from[Yahoo][2] or [MSN][3].[1]: http://google.com/ "Google"[2]: http://search.yahoo.com/ "Yahoo Search"[3]: http://search.msn.com/ "MSN Search"
输出 HTML 为:
<p>I get 10 times more traffic from <a href="http://google.com/"title="Google">Google</a> than from <a href="http://search.yahoo.com/"title="Yahoo Search">Yahoo</a> or <a href="http://search.msn.com/"title="MSN Search">MSN</a>.</p>
title 属性是选择性的,链接名称可以用字母、数字和空格,但是不分大小写:
I start my morning with a cup of coffee and[The New York Times][NY Times].[ny times]: http://www.nytimes.com/
输出 HTML 为:
<p>I start my morning with a cup of coffee and<a href="http://www.nytimes.com/">The New York Times</a>.</p>
图片的语法和链接很像。
行内形式(title 是选择性的):
![alt text](/path/to/img.jpg "Title")
参考形式:
![alt text][id][id]: /path/to/img.jpg "Title"
上面两种方法都会输出 HTML 为:
<img src="/path/to/img.jpg" alt="alt text" title="Title" />
在一般的段落文字中,你可以使用反引号 `
来标记代码区段,区段内的 &
、<
和 >
都会被自动的转换成 HTML 实体,这项特性让你可以很容易的在代码区段内插入 HTML 码:
I strongly recommend against using any `<blink>` tags.I wish SmartyPants used named entities like `—`instead of decimal-encoded entites like `—`.
输出 HTML 为:
<p>I strongly recommend against using any<code><blink></code> tags.</p><p>I wish SmartyPants used named entities like<code>&mdash;</code> instead of decimal-encodedentites like <code>&#8212;</code>.</p>
如果要建立一个已经格式化好的代码区块,只要每行都缩进 4 个空格或是一个 tab 就可以了,而 &
、<
和 >
也一样会自动转成 HTML 实体。
Markdown 语法:
If you want your page to validate under XHTML 1.0 Strict,you've got to put paragraph tags in your blockquotes:<blockquote><p>For example.</p></blockquote>
输出 HTML 为:
<p>If you want your page to validate under XHTML 1.0 Strict,you've got to put paragraph tags in your blockquotes:</p><pre><code><blockquote><p>For example.</p></blockquote></code></pre>
质能方程:
$E=mc^2$
$E=mc^2$
余弦定理:
$\cos 2\theta = \cos^2 \theta - \sin^2 \theta = 2 \cos^2 \theta - 1$
$\cos 2\theta = \cos^2 \theta - \sin^2 \theta = 2 \cos^2 \theta - 1$
插入方程组(注意多行公式结尾\\需要打成\\,可能是因为markdown会自动转义第一个\):
\begin{aligned}\dot{x} & = \sigma(y-x) \\\\dot{y} & = \rho x - y - xz \\\\dot{z} & = -\beta z + xy\end{aligned}
\begin{aligned}
\dot{x} & = \sigma(y-x) \\
\dot{y} & = \rho x - y - xz \\
\dot{z} & = -\beta z + xy
\end{aligned}
插入矩阵:
\begin{bmatrix}1 & 2\\\3 & 4\end{bmatrix}
\begin{bmatrix}
1 & 2\\
3 & 4
\end{bmatrix}
对于Markdown有转换冲突的,可以使用rawblock
将公式扩起来(百分号为%
):
{百分号 raw 百分号}$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$${百分号 endraw 百分号}
$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$]]>专心做好一件事,慢慢来,比较快。
See also, My old blog in csdn.here
Hello World
]]>useradd –d /home/youtpath yournamepasswd yournamegroupadd yourgroupchown yourname:yourgroup /home/yourname
if accuont is exist, try to using usermod -d /home/yourpath -U yourname
(/etc/network/interfaces)
# Dynamic IPauto eth0iface eth0 inet dhcp# Static IP auto eth0iface eth0 inet staticaddress 192.168.33.201netmask 255.255.255.0gateway 192.168.33.1
then, in shell(also, need sudo):
ifconfig eth0 downifconfig eth0 up
last, try to ifconfig
to check result, perhaps you need using /etc/init.d/networking restart
to restart all network services
you need modify 3 files:
/etc/sysconfig/network/etc/sysconfig/network-scripts/ifcfg-eth0/etc/resolv.conf
1./etc/sysconfig/network
NETWORKING=yesNETWORKING_IPV6=noHOSTNAME=keroGATEWAY=192.168.1.1
2./etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0 NETMASK=255.255.255.0 IPADDR=192.168.1.88 BOOTPROTO=static #【none | static | bootp | dhcp】ONBOOT=yes #【yes | no】引导时是否激活设备DNS1=211.99.25.1 #域名解析服务器PEERDNS=yes
3./etc/resolv.conf
nameserver 211.99.25.1 #DNS配置 同2中的 【DNS1=211.99.25.1 】
4.Resart
/sbin/ifdown eth0
/sbin/ifup eth0
/etc/init.d/network restart
sudo apt-get install openssh-server
find ./ -name '*.*'
du -h --max-depth=1
Using ifconfig
check your IP and eth
eth1 Link encap:Ethernet HWaddr 08:00:27:71:DA:8C
inet addr:XXX.XXX.XXX.250 Bcast:XXX.XXX.XXX.255 Mask:255.255.255.0inet6 addr: fe80::a00:27ff:fe71:da8c/64 Scope:LinkUP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1RX packets:782 errors:0 dropped:0 overruns:0 frame:0TX packets:91 errors:0 dropped:0 overruns:0 carrier:0collisions:0 txqueuelen:1000RX bytes:77624 (75.8 KiB) TX bytes:14476 (14.1 KiB)Base address:0xd240 Memory:f0820000-f0840000
Set the Virtual IP,
ifconfig eth1:ha1 XXX.XXX.XXX.252 broadcast XXX.XXX.XXX.255 netmask 255.255.255.0 up
eth1:ha1
, eth1:XXXX, XXXX is your virtual name, modifiy name what you want.
XXX.XXX.XXX.252
, Virtual IP
XXX.XXX.XXX.255
, broadcast (Same as eth1 Bcast)
255.255.255.0
, netmask (Same as eth1 Mask)
Also, you can using ifconfig eth1:ha1 XXX.XXX.XXX.252 broadcast XXX.XXX.XXX.255 netmask 255.255.255.0 down
shutdown Virtual IP
After it, you can using ifconfig
, check result
eth1:ha1 Link encap:Ethernet HWaddr 08:00:27:71:DA:8C inet addr:XXX.XXX.XXX.252 Bcast:XXX.XXX.XXX.255 Mask:255.255.255.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Base address:0xd240 Memory:f0820000-f0840000
arping -c 3 -I eth1 -s XXX.XXX.XXX.250 XXX.XXX.XXX.252
-c
is count,
-I
is your eth interface name,
-s XXX.XXX.XXX.250
is your now ip(source ip)
XXX.XXX.XXX.252
is the virtual ip you want to check
route add -host XXX.XXX.XXX.252 dev eth1:ha1
大一,刚开学那阵子,大家好像延续了军训时期的斗志,都是充满斗志地打满了鸡血。希望加入各种各样的学生组织、社团、班委锻炼自己的能力,希望白天在图书馆、自习室争当学霸,希望晚上在篮球场上、操场上增强体质。当时的心态就是,要做一个德智体美劳全面发展的大学生!(好吧,好假,囧)
在刚入学的时候,就听说了通院科协是一个很锻炼人的组织,于是,招新的时候自信满满的去面试,很不幸,悲剧了,当时真心是感觉人生一下子灰暗了,真没言重,当时就是那么想的。还好通院科协的一些活动、讲座是面对全院的,我十分积极的参加各种各样的讲座、培训,就这样,学习了单片机和编程方面的基础知识 。
想想当时大一真是个鸡血少年,早上 6点多起床,去教室看看书,中午也不睡觉,就去图书馆。大一上学期什么也不懂,大学之前接触过一些编程但是都是很简单的,可以说基本不会;硬件方面也什么也不会,连烙铁都没有用过,开学没几个月就参加了大学的第一个比赛,星火杯。星火杯是学校内的比赛,很有西电的特色,主要就是软件、硬件、软硬结合、论文几个方面。
当时我们什么也不会,就按照电路图,买元件,焊电路,完成了我们的作品——电子百灵鸟。当时,特别特别兴奋,最终得了一个安慰奖。
大一下学期,科协让我们自己动手焊的单片机实验板。这个开发板也变成了我的“启蒙老师”,抱着电脑认真的学习郭天祥《十天学会单片机》,然后自己烧些程序在自己的开发板上。
刚开始的时候大家都有些像没头苍蝇,这时,可以适当地去听听讲座,参考一下长者的意见,只是参考,你应该有自己思想,追随自己的内心,走自己的路。另外,推荐大家读下开复老师的“当迷茫在大学里泛滥成灾”这篇文章。
大二的时候,自己算是入门了,因为特别喜欢编程,所以自己认真学习了 C和 C++,第二次星火杯可比第一次强多了,我们做了一个以公寓远程报修为功能核心功能的作品——基于 ZigBee网络的公寓自动化管理系统,最终得到了评委的肯定,获得校级一等奖。这次比赛算是大学里面第一个认真准备的比赛,开始的时候真心是什么都不会,只学过一些 C/C++,没用过MFC ,没用单片机做过系统,短短的 2个月,从 PC端的管理软件到学生端的交互终端,都是自己认真完成的,确实得到了锻炼。也许很多同学都会想“自己什么都不会,什么也没学过,自己可以吗?”其实大可不必担心,因为你会在你一点一滴的实践中逐渐成长。
在大二下学期恰好有个机会参加了陕西省挑战杯,由学校选拔参加的,基本上和星火杯的流程差不多,我们的作品也没有什么改变,只是完善了文档。文档其实是比赛中很重要的一个环节,好的文档确实能为你的作品添色不少。最终,拿了陕西省挑战杯的二等奖。其实大一大二的时候,就好似一个跳板,为之后打下基础。
大一大二的时候都比较纠结,很多人都纠结自己该怎么走完接下来的路,其实,我觉得吧,不管干什么,只要认真的一心一意的做下去,坚持下去都可以成为一个出色的人。
在大二的六月份左右,学校会组织申报“国家大学生创新创业实验计划项目”也就是我们平常简称的“国创”。通过“国创”的申请,学校会批给你们项目组一万元左右的资金去完成一个项目,时间期限是 1年,每个项目组三人。通过这个机会,你可以“免费”地阅读、购买到很多的书籍,这更是一个难得的机会去真正地接触一个项目从产生、管理、完成的整个过程。当时我们选择做的软件项目,主要是完成了条码扫描、社交分享、附近交易的一个Android 端的软件。很幸运的我们拿到了所有项目组的第六名 (前五能保研,恩,对,前五!杯具! )。
其实,当时也真的不是很在意,因为那时候,已经在嵌入式比赛最后阶段,这个比赛全名叫做“全国大学生电子设计竞赛嵌入式邀请赛”,校队选拔相对比较严格、跨度时间比较长,基本是大三再加上大三暑假就搭进去了。这个比赛是我最投入,最用心也是得到最多锻炼的一个比赛。
2个多月,三轮选拔,从全校范围内 200多人中,选出了 12个来自不同学院的学生,组成 4队进行比赛。因为比赛平台的特殊性,是 Intel最新的CPU ,将FPGA芯片和 CPU集成在一起。从寒假开始,就选题,定题之后就开始完成作品了,每天真的是很充实。我们的题目是,室内交互设计游览系统,通过手上的一对指环,在家居场景中布置家具,体验游览家具设计,在 cpu中完成软件部分,在 FPGA中完成手势识别算法。因为新的东西比较多, Qt、图像引擎、物理引擎、图像识别、虚拟现实。从三月起基本每晚就睡的很晚,到最后比赛接近尾声的时候,基本上每天 3、 4点睡,早上 8点不到就起来了。每天睡的时候,都会和队友开玩笑,“大神们,赶紧睡吧,小心猝了”。比赛那几天更夸张,因为要去上海进行比赛坐火车,硬座一晚上基本没睡,第一天到上海没休息就去抽签,雨舟大神抽了个第二天早上的第三个,当时的心情就一个词儿——情何以堪。于是,第一天晚上也就没睡。那天夜里,突然 FPGA失灵了,当时吓了我们一身冷汗,当时脑海就是一片空白,心想“不是这么悲剧吧?”还好最后,把线重新插了下,又恢复了。付、谢、任三位带队老师特别负责(致敬!),晚上 3点多的时候,还在为我们把关答辩。回去的时候 4点多了,我修改 ppt完了就凌晨 5点了,老师让第二天答辩的人早点睡,因为是我答辩,于是我就可耻地睡去了,君朋大神活生生地一夜没睡,早上 6点多我起来的时候,看君朋还在调指环,鞠躬致敬!
第二天去比赛的时候,我们本来是第三个,第二个放弃了,我们又提前了一个,我们一去就开始准备。去上海的时候,我们准备了四个指环,两对,有一个短路了,当时我就想到一句话“所有你觉得可能出现问题的地方,就会出现问题”,当时也没想太多,比赛的时候,果不其然,悲剧了。那个没有备份的指环,程序跑飞了。君朋大神确实淡定,冷静地把程序重新烧了一遍,又恢复正常了,那就是一个心惊肉跳。最终演示的时候效果还不错,最后拿了国家二等奖,四队最终以一个一等奖,三个二等奖为比赛划上了句号。
我是这样一步一步走来的,当然上面的一些经历只是我个人的情况,刚入大学的你,应该有自己的道路。你需要清楚竞赛的路途上,不只有掌声和鲜花,或许也会有低靡,因为竞赛失利的人大有人在。不过,如果你真的喜欢竞赛带给你的锻炼与能力上的提高,而且在你深思熟虑过自己是否应该参加竞赛,如果确定要继续,那么坚持地做下去,不要在乎太多的功利,因为能力上的提高远远大于那些奖项。或许,你可以尝试从大一起就开始做自己的简历,每学期更新一次,看看自己是否有东西可写,是否比之前有些进步,哪怕只是简历上的一行。
最近看到过一段话,“在一定程度上,大一遇到什么水平的学长学姐,可以左右他大学的方向。请某些人高抬贵手,不要把你那些可笑的经验云云装的很像样的告诉他们,影响人家的认知,大学是他们自己的,走错了路,这个责任谁也担不起”。确实是这样,你需要谨慎地去面对那些“学长学姐”所谓的经验,就好像这篇文章一样,肯定会有不妥的地方,你需要保持一颗怀疑的心,大步向前。当然,如果你遇到了一些自己解决不了的问题,不妨请教一下长者,无论是学长、学姐、导员、老师,他们都会帮助你的。
大学是你们自己的,需要你们自己去探索,我们也只是起到抛砖引玉的作用,追随你自己的内心,走自己的路。准备好了吗?让你的大学流光溢彩吧!
]]>和宿舍同学的关系也越来越和谐了,宿舍的孩子们最近也开始堕落了,天天晚上都在dota,每次看他们玩的时候总想到四个字,玩物丧志。当然,自己在他们玩的时候,也没做些什么有意义的事。甚至周六周日也和他们沦陷一番。
最近和娱乐有关的活动,除了对吃感兴趣外,真的什么也觉得没意思,游戏没心思玩,篮球很久没碰了,电影看一半就想睡觉了,上网看见人人上那些个无聊的状态就直接注销了,不想再上,今天又有急于联系的人,又解封了,我也真是可笑。
今天晚上,突发奇想到操场跑了几圈,在草坪上,休息的时候捡到一个手机,然后,高尚的还给人家,他还说要请我吃饭。囧不囧
最 近,各方面都还顺利,国家大学生创新性实验计划申上了,批1.2万的可用资金,这是第一次有那么多钱可以用吧,好好珍惜,项目期限,一年的时间。富士通的 初赛也通过了,也可以有个开发板可以免费用了,说不定十月的时候还有机会去次上海。世园会志愿者也选上了,暑假也可以充实起来了。
最近压力比较大的还是课内的东西还有六级,现在都是全裸状态。得赶紧补了
最近,也在关注一些实习生招聘的东西,突然觉得涉猎有些广泛了,android&java 嵌入式 c&c++,得精通一个才好。
有一个小小的目标就是大三暑假大四左右的时候,去创新工厂实习。可能android 和C++会成为蓝筹吧。所以,要专心技术基础了。
嗯。拿得起,放不下。是不是很失败啊。哈哈,不知道该怎么办。只怪时间不是优质的稀释液。
有2,3个月都没吃胡辣汤了,这周一定要回去吃!
嗯,就到这里吧。
Go on !
]]>这学期的课都不是什么省油的灯,数电模电数据结构还算过得去电磁场与电磁波真是一门见不到底的学科,信号看着那些卷积积分傅立叶Z变换就头大。考试月也不 会轻松吧,更何况又是万恶的夏天。最近看些模拟技术的东西觉得好有兴趣,可是看着模电书里干巴巴的式子也顿时没了激情,这还是因为过于浮躁吧。
天气最近也是格外的变态,又听到了一些同学问类似于西安天气是不是一直这么怪的问题。我也是不知怎么回答的笑笑,天气也不归我管吧,是吧。
大家也都在开始为着自己的事情奋斗了,拼命GRE奋斗雅思的人也不少,像我吧,这种飘摇不定的不知上研还是工作,只是一直在学着,51完了430,430中间又杀出个cortex M3,无奈又转战ARM了。就等着厚积薄发,独孤一掷了吧。
慢慢的,一些事情和能力也渐渐的得到了认可,自己也有了那么几分信心。 当然,还是时常被一些繁琐的事困扰着,侵略着不多的自由。还好听说明天可能就发布上线了,也没我这个打酱油的什么也不会的美工什么事了,也算了却了一桩心 事吧。不过这些时间也认识了不少强人,自己也学到了一些UI方面的东西。
有句话说的好,穷则独善GPA,达则兼顾GDP。有时候也翻翻墙看看世界,和同学交流一下真相什么的也挺开心的。最近地球不太安稳,一会地震一会军事冲突的,渐渐的发现自己有些锁闭了,所以开始坚持每天看看新浪,也有了个微博叫Keroenigma。
前几天还看见同学说,大学找到媳妇就工作,找不到就安心上研。这也不失为一个好办法,要不我也这样?其实我还是偏重于工作一些吧。 有些事情,很久,很久。还是没有忘记,或许就像一个划伤,虽然不疼了,但是疤还在那,永远也不退去,都是写死亡的细胞却永远不能消失殆尽。慢慢的真的是越 来越远了。或许这也是没什么办法的无能为力吧。每当想起了就想着,有些事情过去了就让他过去了吧,也没有什么时间和机会去留给我去后悔。
时间啊时间,又是时间。如果要是时间多点,哦,没有如果。
开始学车了,也还算顺利,倒库移库的一直在练。
四级也坎坷的裸过了,六级好好准备吧。
三月的最后几天了,很关键吧。一个人发现自己最平庸的时候便是没有灵感。有时候没有想法真的是一件苦恼的事。
这学期的这些个事也不会闲下来的,这样也好,慢慢来吧,一行一行的写,书一本一本的啃,路一步一步的走。。。
嗯。还是那句话,无悔就行了。
]]>正如二姐所说的,于是,大二了。经过一年的高 等教育的培育,我现在也算是一名不折不扣的大学生了。二姐文章内的很多都是藏在内心里面,不想在公开场面表露的。我自认为我还算是一个敢说敢做敢为的人。 正如他所含沙涉影的描述,我们的大学生活中出现了这种那种利益的纷争。我在考虑具体什么的用不用在这里附上一个超级链接然后转向二姐的日志,但是考虑到二 姐的广泛影响力还有手机的具体能力。我还是决定不多此一举了。关于学生会、奖学金、团委、热水供应那些和我无关的事情我便也无权描述。既然,我处在大学这 个小社会中,那么我也应该享有这么一项言论自由。所以,写不写随我,看不看随你。
大一一年的时间,我到底做了什么,我想也是时候描述一下了。没有什么丰功伟绩,没有什么惊天动地,和大家一样的加了几个什么组织。
那好,先说学生会。为了那些所谓的潜移默化的进步,然后投简历,开始兴致勃勃的进行面试,终于是头破血流般的从外联部调剂到学习实践部,在这个部酱油的一年 来,多多少少也算是有所收获有所成长,以致于我觉得进步超过了我所想象,感觉到所有的东西都领悟了之后,在学期初的时候,选择了退出。刚才看到二姐日志中 写到什么干事—部长助理—部长副部长的路线,突然给了我启发,我才知道原来事情是这样发展的。这里,我也无力去评价学生会什么的好坏,大家心里也都明白。 我是想说,什么如果留下来要给我升官,什么我可以去聚餐那些东西又能影响的到我什么,最多是简历上一行平淡的经历,然后再也没什么了,被大家妖化的学生 会,也在日益增加的光芒中不断的健壮,赢得了大一莘莘学子的青睐,最终学生会也往开一面,基本全额地收留了屈指可数的报名的精英。有些人评价说学生会要没 落了,那些事情与你们无关,你死了以后,不会在你的墓碑刻上一行字:在学生会没落之前,此人果断离开。所以,我也没有说我的离开彰显我的明智。只是个人不 喜欢那种氛围罢了。什么大二之后就可以管别人了,我自己的事情都没有管好,哪有资格倚老卖老欺骗广大新生,并且美其名曰潜移默化的进步。
然后是科协,当我斗志昂扬的写好简历,面试,并且被面试官无情秒杀时,便发誓与科协誓不两力。如今,我也便是充满自豪的忝列于技术部部长助理的名单中,在 此,我想我要向张静媛同学致谢,没有她的推荐,或许我还在科协外部碌碌无为。比较戏剧的我也成了面试官,当坐到这里,我才发现,或许,那些有意念想要学好 的孩子们,也被我们这些面试官以一句:技术部需要有技术基础的搪塞过去了。科协确实是个好地方,没有学生会那些无聊的纷争,做自己喜欢的事情,学学技术, 打打酱油,足以。学长们都有闪闪发光的一面,没有什么架子可言,什么嵌入式奖杯,什么国创计划,有能力的学长有着这样那样的荣誉,可是没有丝毫的高姿态, 我想这才是作为学长应有的姿态。尤其要提下黄翔学长,前几个月为了给我讲一个8255的读写,花费了他将近三个小时。最后还从他口中得知,当时他在给我们 讲单片机的时候,被不少人误以为他们从实验板里捞了不少钱。我说那些人你是脑残还是怎么了叫你大四保研了,占用自己的时间去教大一的,不求回报,你能做到 么。别以为你不能,别人就不能。在这里还是要非常感谢那些学长的,当然,到大四以后,如果我也有能力的话,也一定会传承你们的意志的。
下来说说星火杯,什么我们这组好强,什么冲击校特,都是吓唬人的。不管这组有怎样怎样栩栩如生,精彩动人刻骨铭心的故事,那些都不重要,重要的是在于真正的 学到东西了,这种事情过程确实很重要。和强人混,拿别人的东西ctrl c,ctrl v,然后你拿个院奖免修,或者一不小心还拿个校特,保研加个1分2分,对自己又有多大提升?什么没基础求强人带,你就那么看不起自己?那么强人永远是强 人,你自己永远只是你自己。我没有像某些人什么从小就开始学编程,家里的人随便就弄个专利给自己挥霍。我就是零基础那又怎样,我通过自己努力,团队合作完 成了作品,达到了自己预想的功能,这就足够了。对自己的队友的能力要信任,这是最起码的规则,你必须要遵守。所以,到最后,不管是没奖院奖校奖,不管合理 与否,我都接受,我也只能接受,至少我能做到问心无愧。那些纷争,利益,关系,那些都是社会层面的事情,和技术层面是没有任何牵连的。
下来说说班级,我通过匪夷所思的路线,从心理委员一跃成为副班长,可能大家会想,凭什么我就拿了社会工作奖学金,凭什么我就有资格参加高党课程。得到这些东 西或者荣誉,我觉得没人会认为我是绞尽脑汁用什么非法手段吧?这点我还是比较自信的。下来就是班长竞选,王琪他大一做了多少事可能我是比较清楚的,为了一 个答辩,你能做到耗尽自己自习的时间吗?虽然,你对他这样那样的看不惯,不过你想想也便明白,为什么王琪最终还是当上正班长,即使只是一票之差。
至于导员,大家对导员有这样那样的意见,在这里我也无意评价导员的好坏。导员本身是个比较尴尬的位置,对于班主任大家都心存一个黑暗的形象,以致于把这层阴 影覆盖到了导员的职位上,我只是希望大家在说一些话的时候,能换位思考一下。人无完人,或许有些事情处理的不得当,但是,请你不要用有色的眼睛去看待世 界。好了,夜也比较深了,想说的也说完了,打字也累了,那么,就到这吧。
]]>没有犹豫的拿起钢笔,已经干涸了的钢笔,吸了几滴甘霖般的墨水,蓝色,纯蓝色。坐在马扎上,靠着阳台的墙,墙被太阳烤得有些余温,靠上去丝毫感受不到温暖, 而是几分燥热。外面是垃圾与绿色的杂草构成的废地,夏的绿很深很深,在几处凌乱的垃圾的点缀下略显几分荒颓,这样的荒颓如大一下学期的生活,没有一丝一抹 的鲜活的灵感,只剩下没有生机的暗淡;废地旁,还有一个废弃的工厂,传说之中有各种原因,它坚强的被遗弃在这荒芜的西电,工厂只剩下一圈围墙禁锢些那些荒 砖颓墙,几簇挺拔的野草顽固的扎在那儿,又增添几分紊乱,这紊乱如大一下学期的林总,无处安置的梦想碰到了迷失的现实。
坐在阳台最显惬意的是能吹来一阵阵的微风,触摸灵魂般的微凉。一切都很安静,很安静。安静到坐在这里什么也不用想,那些思绪便仿佛识破了这安寂一般,喷涌而出,没有人打扰我,没有人能打扰我,没有人会打扰我,略显几分得意的孤独。
是 孤独,不是寂寞。孤独是一种比寂寞更坚强的东西,没有寂寞那么的无助。身边,没有人说话,没有人谈心,即使有,也是肤浅的嘻哈几句带过,记着开学的时候, 总把贵重的物品,还有这些珍贵的笔迹锁在抽屉里,有戒备。现在呢?锁永远开着,没有人会动那些所谓的贵重,或许,就算是不小心看见了这些凌乱的笔迹也可能 看不懂吧?这锁确实永远开着,然而,心却上了一把锁,没有钥匙的锁。
我曾说过,本来人与人之间并没有隔阂,可是,门一层一层地加上了,锁也一把一 把地死死地焊在上面,于是,太累了,便懒得去打开,或者是说不想去打开。眼看着日子就这么一天一天的从眼间划过,也便慢慢地,力不从心,无力抓住,毫不知 情地被几分低迷侵占,然后守着梦想糜烂在这无处安放的青春。
想要破除这阴霾,却总是半途而止。再看看天,那略显稀薄的云雾也开始慢慢地合上,几缕阳光是否也望见了这世间的暗淡,无奈地退了回去?
客 厅又传来了咯吱的的门声,再啪的一声彻底击碎这片宁静,回头看看,不是有人回来,而是有人离开。离开?是的,就像朋友一个一个离去一样,不打招呼,还没反 应过来,这里便空得只剩下我一个人。刚才又看到了类似的话语。“朋友。一个,知己;两个,略显敷衍;三个、四个,也未免太多了吧”有到了一个我可以笑而不 语的时刻。每次,每时,每刻听到朋友二字时,心中总是有说不出来的无奈与酸楚,我人际能力太强了吧,人缘太好了吧?呵。。大家都说朋友是财富。那我未免也 有点太过富有了吧?富有到最后,我便应该成了一个最穷的富翁了吧?
是不是想得太多了,也罢,该收尾了。顺其自然。这些思绪或许也就停留在这些歪七 扭八的字里行间了吧?想想也可笑,高中文章结尾的时候总喜欢这样写:我拾起散落了一地的思绪,看着远方的阳,夕阳咬破云的唇,留下一抹血迹在天边。”然后 便是描写几句明早破晓的美好。可现在呢?却真是到了夕阳时,,天际却还是只有大片的阴霾,思绪也不那么能轻易的散开,而却像墨迹留在纸上一样,便深深的印 在了我这矫柔造作的文字上了。还好这不是高考作文,我不用可以去敷衍,隐藏什么。现实是怎样我便记下来。
远处还是阴霾,破晓是否能见朝阳,谁可得知。正如我于别人,永远是个Enigma。
]]>