跳转至

2020

Linux 中断请求

中断请求的英文是IRQ(Interrupt Request),是用来驱动CPU正常工作的重要机制。 中断根据源头分类成: 由外设发出的硬件中断,软件发出的软中断,以及异常组成。

以前,每个外设都连接一根线到PIC(programmable Interrupt circuit)芯片,有PIC芯片来发生数据到CPU,并将CPU的INTR引脚置位。 现在,CPU集成了PIC成为APIC(Advanced PIC)。

中断分类

中断分为3类型

硬件中断

鼠标和键盘,还有IO设备都会发出硬件中断,例如网卡的数据到了,就会出发中断请求(IRQ), 这个请求会触发CPU去执行ISR, 而这个ISR需要在系统启动的时候注册的。一个Vector表。

软件中断

当播放电影的时候,声音和画面的同步非常重要,这是由系统的定时器jiffies来不停地调度声音播放器来准确播放对应的声音。 软中断在实时操作系统的作用也非常重要。

异常中断

异常中断又分为3类: 缺页异常(Faults),陷入异常(Traps),异常退出(Aborts)

例如当程序的内存被swap到硬盘了或者一段动态链接库还没有载入到内存里来,在程序走到这个位置之前, 程序就会提前发出缺页异常,当系统检查了权限和地址有效后(地址有效表示在逻辑地址范围内), CPU开始为程序执行加载需要的数据,并清除这个异常。 所以这个异常是可以修正的,这个请求必须提前发出。

当GDB调试的时,设置断点会插入一条特殊的指令到程序里,当执行到断点位置就会出发Traps请求,软件的主导权由CPU交给GDB。

当程序触发一个异常例如0/0时,会出发退出异常,由cpu来清理堆栈。

请求的可阻塞性

对于硬件中断请求,CPU的决策是立即执行,对于软中断和异常,CPU采用尽量延后的策略。

查看系统的中断

在中断执行 watch -n1 "cat /proc/interrupts"可以定时更新中断的次数

列的含义从左至右以此为:

 IRQ vector, interrupt count per CPU (0 .. n), the hardware source, the hardware source's channel information, and the name of the device that caused the IRQ.

记录一下上课笔记

  1. 进入nfs,autofs目录卡住,如何退出,应该是进入了D状态
  2. chrome 需要笔记本插件,js控制插件
  3. 修改root密码时,在linux 选项后面添加 rd.break 然后默认进入内核根目录,然后rw重新挂载系统根目录 /sysroot/
  4. fdisk 设置分区类型: 在创建分区结束后,使用t来标记文件类型, 然后使用L查看类型代码,查找到Linux swap 对应的代码是85

RHCIA

修改了网络信息之后,必须使用nmcli connection down和up connection才会生效

nmtui connection down Wired_connection1
nmtui connection up Wired_connection1

配置httpd服务时,修改port后,因为port非标准端口,需要用semanage打tag。否则失败,可以查看journeyctl -e 查看到错误信息和解决办法

semanage port -a -t http_port_t -p tcp 82

创建/home/admins 目录,并设置在目录中创建的文件,其组所有权自动设置为 adminusr。 这里就是设置s位,有数值方法和+-法

chmod 2770 /home/admins

# 或

chmod o-x /home/admins
chmod o-r /home/admins
chmod g+w /home/admins
chmod g+s /home/admins # 这里设置组的s位

验证方法,进入目录创建文件,查看组名

autofs 挂载有两个关键,第一在/etc/auto.master.d/ 目录下创建后缀名为autofs的文件, 第二是编辑挂载配置文件

内容格式

# cat /etc/auto.master.d/indirect
#主挂载点   挂载配置
/internal   /etc/auto.demo

内容配置,* 表示所有目录(根据nfsserver的情况), 和&搭配使用

# cat /etc/auto.demo
# 间接挂载目录    挂载配置    挂载路径(nfs地址)
*   -rw,fstype=nfs4,sync    servera:/shares/indirect/&

setfacl 和getfacl 设置和显示文件属性

设置natash用户读写权限
setfacl u:natasha:rw    /var/tmp/fstab
设置harry用户没有任何权限 
setfacl u:harry:-   /var/tmp/fstab

重置root密码,在linux 行末追加 rd.local, 按ctrl+x 启动

重新挂载系统根目录, chroot到根目录和修改密码

mount -orw,remount /systemroot/
chroot /systemroot/
passwd

逻辑卷扩容

设置分区start位置的时候,输入2048s。 s指扇区,一个扇区为512字节, 2048s指1M,这1M的分区为esp分区

扩容命令lvextrend,这个扩容只是将逻辑卷扩容, 而题目要求对文件系统扩容, 还需要执行文件系统类型对应的扩容命令如resize2fs(ext), xfs_growfs(xfs)。 但对lvextend命令添加 -r 参数可以一步到位

# 扩容到300MB
lvextend -r -L 300MB /dev/new_redhat/lv1

创建逻辑分区, 注意避免磁盘不够用,尽量共用上一题的磁盘。而且创建卷组的时候不要忘记-s参数

题目可能不是直接给分区大小,而是给出扩展块大小和拓展块个数,其实分区大小=拓展块大小 × 拓展块个数。 在lvcreate -l 来指定块个数

这时必须使用msdos分区类型才有主分区和拓展分区,而且为拓展分区分配剩余的所有空间。 最后在拓展分区上创建逻辑分区, 并将创建的逻辑分区加入卷组

创建分区,大小通过上面的算法计算出来

parted 
mkpart
print
exit

创建卷组

vgcreate -s 20MB -n npgroup /dev/vdb2
创建逻辑卷, 注意是小写l
lvcreate -l 45 -n np npgroup
这样就创建了一个 20×45 = 900MB的逻辑卷

创建新的swap分区,注意需要修改fstab, 而且验证的时候,不需要重启系统,而是执行

systemctl daemon-reload 
swapon -a  # 类似mount -a
swapon --show 

vdo --vdoLogicalSize 5G, 不能使用GB

vdo create --name test --vdoLogicalSize 5G --device /dev/vdb
blkid 查到 UUID, 最后添加到fstab

vdo 持久挂载 mount

default,x-systemd.requires=vdo.service

RHCE

ansible的有用命令 ansbile-doc -l | grep 查找模块和介绍 ansible-doc 模块 查看模块具体说明

创建inventory 文件, 子组的写法为

[webservices:children] prod

prod 是webserverices组的子组

创建ansible.cfg 文件,可以复制 /etc/ansbile/ansible.cfg 到~/ansbile/目录下

容易忘记的选项是远程登陆身份, 即配置 remote_user = devops 如果环境没有配置免密登陆,需要在[defaults] 栏添加 ask_pass = True

验证方法 ansible all -m ping

ansbile ad-hoc 添加yum 源 ansible 模块的使用方法 ansible 主机名 -m 模块 -a '参数'

脚本要添加可执行权限

#!/bin/bash

# 先要切换到工作目录
cd /home/devops/ansible/

ansible all -m yum-repository  -a 'name=  description=  baseurl=  enabled= gpgcheck= gpgkey= '

安装软件包

安装软件包如'Development Tools', 需要在包名前面加'@'即 '@Development Tools' 验证yaml 语法格式: ansible-playbook --syntax-check *.yml

- name: install  development tools
  hosts: dev
  tasks:
    - name: install development tools
        yum:
          name: "@Development Tools"
          state: present                  

同步时间依赖角色,安装角色包'rhel-system-roles' 需要在配置文件里添加上roles路径

playbook 示例在/usr/share/ansibe/roles/rhel-system-roles.timesync/Readme.md 现成的yml示例在/usr/share/doc/rhel-system-roles/timesync/example-timesync-playbook.yml

# ansible.cfg
[default]
roles_path = /home/devops/ansible/roles:/usr/share/ansible/roles

使用ansible-galaxy 命令安装角色,安装到本地roles目录

yml文件很简单, 执行命令 ansible-galaxy install -r roles/requirement.yml -p roles/ -p 表示安装路径

- name: balancer
  file: /home/devops/ansible/files/5/haproxy.tar

创建apache角色, 角色对应一系列的任务和模板文件

通过ansible-galaxy init apache 来初始化角色目录,修改角色目录下的tasks/mail.yml 来添加任务,任务所需的模板文件在同级目录templates下
通过ansible servera -m setup 来查询环境变量, 在模板文件中使用ansible_facts['fqdn'] 来引用变量 题目说的hostname 是serverc.lab.example.com 对应变量是fqdn, 另外一个是ip即 ['default_ipv4']['address']

例如,安装httpd用到yum模块, 启用httpd 用到service模块, 防火墙配置用到firewalld模块

# home/devops/ansible/role/apache/tasks/main.yml

---
- name: install httpd
  yum:
    name: httpd
    state: present

- name: enable httpd
  service:
    name: httpd
    state: started
    enabled: yes

- name: firewalld config
  firewalld:
    service: httpd
    immediate: yes
    permanent: yes
    state: enabled

- name: install templates
  template:
    src: index.html.j2
    dest: /var/www/html/index.html 

模块文件放到templates目录下

# home/devops/ansible/role/apache/templates/index.html.j2
Welcome to {{ ansible_facts['fqdn'] }} on {{ ansible_facts['default_ipv4']['address'] }}

最后还需要创建一个playbook, 如同其他playbook 一样执行

# newrole.yml
---
- name: install new role
  hosts: webservers
  roles:
    - apache

执行balancer 角色, 创建一个负载均衡

验证: ssh到serverd 上,curl localhost, 先是访问的serverb, 然后访问的serverc

---
- name: deploy apache on webservers
  hosts: webservers
  roles:
    - apache

- name: deploy balancer on balancer
  hosts: balancers
  roles:
    - balancer

- name: deploy phpinfo on webservers
  hosts: webservers
  roles:
    - phpinfo
可能出现的错误,提示应该使用bind,这是由于balance role的template 错误

修改办法, 将listen行换行并加上bind

# /home/devops/ansible/roles/balancer/templates/haproxy.cfg.j2

backend app
  {% for host in groups.balancers %}
-   listen {{ daemonname }} 0.0.0.0:{{ listenport }}
+   listen {{ daemonname }} 
+   bind 0.0.0.0:{{ listenport }}
  {% endfor %}

创建逻辑卷,考验block/rescue/always, 使用lvol,when 和play对齐

使用了block/rescue/always就不需要tasks关键词

---
- name: create a logic volume with size of 5000 and format
  hosts: all
  block:
    - name: create logic volume
      lvol:
        vg: research
        lv: data
        size: 1500
    - name: fomat data volume
        filesystem:
          fstype: ext4
          dev: /dev/research/data
  rescue:
    - name: debug message
      debug:
        msg: can't create a size of 1800
    - name: create a logical volume with size of 800 and format
        lvol:
          vg: research
          lv: data
          size: 1500
    - name: fomat data volume
        filesystem:
          fstype: ext4
          dev: /dev/research/data
      when: ansible_facts['lvm']['vgs']['research'] is defined
      ignore_errors: yes
    - name: debug 2 
        debug:
          msg: volume group does not exist
      when: ansible_facts['lvm']['vgs']['research'] is undefined

创建hosts文件, 考验jinija 的for循环和读inventory文件

取facts变量的方法参考balancer的模板: /home/ansible/roles/balancer/roles/template/haproxy.cfg.j2

基于给的hosts.j2 文件修改, 拼凑出ip url alias格式,分别对应 default_ipv4.address, fqdn, hostname
ansible_facts在hostvars[host]中

# 取
{% for host in groups.all %}
{{ hostvars[host]['ansible_facts'] *忽露不写了* }} × × 
{% endfor %}
--- 

创建playbook的要小心,题目要求生成所有主机的纪录,所以hosts是all, 但只部署在dev组的主机上要加when,   
要加`when: 'dev' in group_names`

```yml
---
- name: gen hosts and deploy on dev group
  tasks:
    - name: gen hosts and deploy
        template:
          src: hosts.j2
          dest: /etc/myhosts
      when: "'dev' in group_names"

创建web server, 注意将文件标注setype为httpd_system_content_t

通过man semanager-fcontext 查询setype, 或者使用ls -ldZ来查询selinux标签

创建文件,目录、链接都用file模块,但操作文件内容用copy模块 创建组用group模块,创建用户用user模块 容易搞反的链接的src是指向的文件,所以是/webdev, 可以看帮助文档

---
- name: create web server
  hosts: webservers
  tasks:
    - name: add group
        group:
           name: webdev
           state: present
    - name: create directory
        file:
          path: /webdev
          group: webdev
          state: directory
          mode: u+rwx,g+rwxs,o-rx
 -        setype: httpd_system_content_t
    - name: create a symbolic link
        file:
 -        src: /var/www/html/webdev
 -        dest: /webdev
 +        src: /webdev
 +        dest: /var/www/html/webdev
          state: link
    - name: create index.html
        copy:
          content: Development
          dest: /var/www/html/webdev/index.html
+         setype: httpd_system_content_t                    

创建硬件报告,用到loops进行数组循环,内置变量item和 regexs匹配,lineinfile 进行文件编辑

---
- name: generate hw report
  hosts: all
  vars:
    hw_all:
      - hw_name: HOST
        hw_content: "{{ ansible_facts['hostname'] }}"
      - hw_name: BIOS_version
        hw_content: "{{ ansible_facts['bios_version'] }}"

创建加密文件,用到ansible-vault命令

ansible-vault encrypt lock.yml --vault-password-file=secret.txt 

添加用户, ansible-playbook 通过 --vault-password-file=secret.txt 来传密码文件

通过vars_files来读取变量文件, loop 来循环遍历数组变量, 然后模板直接取密码后用'| password_hash('sha512')' 来加密密码 通常情况下在playbook中,开头位置引用一个变量时都需要"{{ variable }}", 但是在when块里,则直接使用 variable。 另外字符串要用“string”

创建用户时, 指定附属组用groups, 主要组是group

- {{ variable }} ## 错误
key: {{ variable }} ## 错误
- "{{ variable}}" ## 正确
key: "{{ variable }}" ## 正确

结果

---
- name: create user on dev,test
  hosts: dev,test
  vars_files:
    - lock.yml
    - user_list.yml
  tasks:
    - name: create group devops
      group:
        name: devops
        state: present
    - name: create user 
      user:
        name: "{{ item.name }}"
-       group: devops
+       groups: devops        
        password: "{{ pw_developer | password_hash('sha512') }}"
      loop: "{{ user }}"
-     when: "{{ item.job }}" == developer
+     when: item.job == "developer"
- name: create user on prod
  hosts: prod
  vars_files:
    - lock.yml
    - users_list.yml
  tasks:
    - name: create group manager
      group:
        name: manager
        state: present
    - name: create user
      user:
        name: "{{ item.name }}"
        groups: manager
        password: "{{ pw_manager | password_hash('sha512') }}"
      loop: "{{ user }}"
-     when: "{{ item.job }}" == manager
+     when: item.job == "manager"

修改密码, 使用ansible-vault rekey 修改密码,或者先解密后加密

修改文件内容, 使用copy模块,以及when条件判断

在playbook的内建变量 inventory_hostname 表示定义在inventory里的主机名称
也可以使用ansible_facts['hostname'],前提是被管理节点的hostname必须如果管理节点定义名称一样。 因为后者是到被管理节点获取。

使用when 限定条件, 满足条件才能执行这个task

---
- name: create issue
  hosts: all
  tasks:
    - name: create issue file
      copy:
        content: "Development"
        dest: /etc/issue
      when: inventory_name in groups.dev  

第二次排错

ansible-galaxy 安装角色, 指定方法分别是 src/name
- src: files/5/balancer.tar
  name: balancer
- src: files/5/phpinfo.tar
  name: phpinfo

timesync 看doc 示例, 验证方法是看timedatectl 看system clock syncronize 为 yes

---
- name: use timesync role
  hosts: all
  vars:
    timesync_ntp_servers:
      - hostname: 
        iburst: yes
  roles:
    - rhel_system_roles.timesync

创建hosts, 要用 {% for host in groups.all %} {{ hostvars[host].ansible_facts }} {% endfor %}

使用firewalld模块时,需要指定immediate 参数,确保能立即生效

设置文件的权限时,应使用u=rwx,g=rwxs,o=rw, 而不是u+rwx,原因很好理解

SQL优先级问题

例如数学中的加减乘除,以及编程里的运算符,在sql里也有优先级,依次是:

FROM clause
WHERE clause
SELECT clause
GROUP BY clause
HAVING clause
ORDER BY clause

优先级依次下降,所以FROM的优先级比WHREE高,这样会有什么影响?

优先级的问题

因为SELECT的优先级比WHERE低,所以在SELECT里面定义的alias别称就不能在WHERE 里面使用
但可以在ORDER里面使用,因为ORDER 后执行

常见问题

...
GROUP BY a, b, c
ORDER BY NULL
. . .
GROUP BY a, b, c
ORDER BY a, b, c

在上面的两个例子里的ORDER BY都不会执行,分别解释:

  1. 第一个例子里的ORDER 是删除GROUP的排序
  2. 第二个例子的ORDER是重复了GROUP已经干了的事情

验证方法

可以找个在线SQL工具,例如 http://sqlfiddle.com/

先执行创建数据库命令

DROP TABLE if exists new_table;

CREATE TABLE `new_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`testdecimal` decimal(6,2) DEFAULT NULL,
PRIMARY KEY (`id`));

INSERT INTO `new_table` (`testdecimal`) VALUES ('1234.45');
INSERT INTO `new_table` (`testdecimal`) VALUES ('1234.45');

set @mysqlorder := '';

select @mysqlorder := CONCAT(@mysqlorder," SELECT ") from new_table,(select @mysqlorder := CONCAT(@mysqlorder," FROM ")) tt
JOIN (select @mysqlorder := CONCAT(@mysqlorder," JOIN1 ")) t on ((select @mysqlorder := CONCAT(@mysqlorder," ON1 ")) or rand() < 1)
JOIN (select @mysqlorder := CONCAT(@mysqlorder," JOIN2 ")) t2 on ((select @mysqlorder := CONCAT(@mysqlorder," ON2 ")) or rand() < 1)
where ((select @mysqlorder := CONCAT(@mysqlorder," WHERE ")) or IF(new_table.testdecimal = 1234.45,true,false))
group by (select @mysqlorder := CONCAT(@mysqlorder," GROUPBY ")),id
having (select @mysqlorder := CONCAT(@mysqlorder," HAVING "))
order by (select @mysqlorder := CONCAT(@mysqlorder," ORDERBY "));

在执行 select @mysqlorder; 结果如下:

@mysqlor`der
(null)

可见其优先级顺序为

FROM JOIN1 JOIN2 WHERE ON2 ON1 ORDERBY GROUPBY SELECT WHERE ON2 ON1 ORDERBY GROUPBY SELECT HAVING HAVING

搭建k8s集群

引用 https://blog.piaoruiqing.com/2019/09/17/kubernetes-1-installation/ 照着文章搭建k8s集群,写得挺好,记录一下自己的搭建方法和问题

环境

  1. 2个kvm虚拟机, 安装的centos7系统
  2. 两个虚拟机都配置了双网卡,eth0和eth1, eth0桥接宿主机的bridge, eth1 接入libvirt default NAT网络
  3. 官方推荐配置: CPU > 2,内存 > 2G
  4. eth1 的ip 分别为 192.168.122.61, 192.168.122.161

Master和Worker 共同步骤

需要在所有节点执行

修改hostname

编辑hostname

vim /etc/hostname # 修改hostname
配置host文件, 将所有节点都指定host

关闭防火墙

  1. systemctl disable --now firewalld
  2. 修改 /etc/seconfig, 将selinux 配置成disable

关闭 swap 分区

  1. swapoff -a
  2. 修改 /etc/fstab, 注释掉swap 记录

安装docker 配置镜像源

修改 /etc/docker/daemon.json 文件

{
  "registry-mirrors": ["https://xxxxxxxx.mirror.aliyuncs.com"],
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}

### 添加k8s 软件源

如下添加的aliyun的镜像源

``` cat < /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg exclude=kube* EOF

### 安装k8s

安装组件 
yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes systemctl enable kubelet && systemctl start kubelet
### 配置网络
cat < /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF sysctl --system
上面的步骤需要在主备上执行

## Master 操作 

下面的步骤只在Master节点运行

### 生成k8s初始化配置文件 
[root@k8s-master ~]$ kubeadm config print init-defaults > kubeadm-init.yaml
得到 kubeadm-init.yaml 文件

修改kubeadm-init.yaml 文件
1. 修改 `advertiseAddress: 1.2.3.4` 为本机地址 192.168.122.61
2. 修改 `imageRepository: k8s.gcr.io ` 为 imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers 

修改镜像地址,这样就避免下载k8s 镜像超时

修改后的kubeadm-init.yaml

```yaml
apiVersion: kubeadm.k8s.io/v1beta2
bootstrapTokens:
- groups:
  - system:bootstrappers:kubeadm:default-node-token
  token: abcdef.0123456789abcdef
  ttl: 24h0m0s
  usages:
  - signing
  - authentication
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.33.30.92
  bindPort: 6443
nodeRegistration:
  criSocket: /var/run/dockershim.sock
  name: k8s-master
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/master
---
apiServer:
  timeoutForControlPlane: 4m0s
apiVersion: kubeadm.k8s.io/v1beta2
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns:
  type: CoreDNS
etcd:
  local:
    dataDir: /var/lib/etcd
imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers
kind: ClusterConfiguration
kubernetesVersion: v1.15.0
networking:
  dnsDomain: cluster.local
  serviceSubnet: 10.96.0.0/12
scheduler: {}

下载k8s的组件镜像

kubeadm config images pull --config kubeadm-init.yaml

初始化 k8s

kubeadm init --config kubeadm-init.yaml
执行无误后,得到worker 节点加入集群的命令
...
Your Kubernetes control-plane has initialized successfully!
...
Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837 

记录这个命令, 丢失的话也可以重新执行 kubeadmin create token 找回

配置普通用户执行kubectl 命令

用普通用户执行一下命令, 这样普通用户也能管理集群

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

查看Master 节点状态

可见,Master节点未就绪,这是因为还没安装网络组件

[root@k8s-master kubernetes]$ kubectl get node
NAME         STATUS     ROLES    AGE     VERSION
k8s-master   NotReady   master   3m25s   v1.15.3

配置网络

  1. 下载calico 描述文件 wget https://docs.projectcalico.org/v3.8/manifests/calico.yaml

  2. 修改calico 描述文件里的serviceSubnet,修改为跟kubeadmin-init.yaml一致

    cat kubeadm-init.yaml | grep serviceSubnet:
    serviceSubnet: 10.96.0.0/12
    

注意
1. calico 的地址必须和 kubeadm-init.yaml 保持一致, kubeadm-init.yaml 默认为 serviceSubnet: 10.96.0.0/12, 而calico的默认地址为192.168.0.0/16, 所以要么修改kubeadmin-init.yaml, 要么修改calico.yaml。 2. 这里的Subnet 不能涵盖到主机的地址范围即不能包含 192.168.122.0/24

安装calico

kubectl apply -f calico.yaml 

查看Master节点

当calico 安装好了, 可以发现Master 节点变为就绪, 至此Master 节点就配置好了

[root@k8s-master ~]$ kubectl get node
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   15m   v1.15.3

安装dashboard

之前的步骤安装好了Master节点, 可选择安装dashboard,通过网页来管理集群

步骤很简单, 先下载dashboard, 再安装

[root@k8s-master ~]$ wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
[root@k8s-master ~]$ kubectl apply -f recommended.yaml 

创建dashboard 用户

必须创建用户,然后获取token,才能在集群外访问, 否则必须在Master 的localhost 访问dashboard。 以下是创建用户的描述文件, 注意namespace, 例如官方的例子是 dashboard, 这样生成的token 不一样

执行安装用户 kubectl apply -f dashboard-adminuser.yaml

# dashboard-adminuser.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kube-system

创建证书, k8s 不允许远程使用http,所以需要用证书访问dashboard

[root@k8s-master ~]$ grep 'client-certificate-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.crt
[root@k8s-master ~]$ grep 'client-key-data' ~/.kube/config | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.key
[root@k8s-master ~]$ openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out kubecfg.p12 -name "kubernetes-client"

将生成的 kubecfg.p12 文件导入到集群外的主机,即kvm宿主机上

scp root@10.33.30.92:/root/.kube/kubecfg.p12 ./

在宿主机上使用chrome 的高级功能里可以导入证书

重启chrome 登录后登录

https://192.168.122.61:6443/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login

打开网页后选择输入token, token通过在Master 节点执行一下命令生成, 注意参数 -n kube-system, 需要跟创建用户时的namespace保持一致

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

复制token 到网页上,就能登录Dashboard

Worker节点加入集群

直接在worker节点执行

kubeadm join 10.33.30.92:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:2883b1961db36593fb67ab5cd024f451b934fc0e72e2fa3858dda3ad3b225837

然后到Master 节点执行以下命令, 这里Name 显示了k8s-worker是因为我们在Hosts文件里指定了ip,所以k8s自动识别到了,否则会显示节点ip

[root@k8s-master ~]$ kubectl get node
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   10h   v1.15.3
k8s-worker   Ready    <none>   96s   v1.15.3

至此集群搭建完毕

遇到问题

  1. 因为我的虚拟机配置的双网卡, 需要在calico.yaml 配网卡interface
    # Cluster type to identify the deployment type
                - name: CLUSTER_TYPE
                  value: "k8s,bgp"
                - name: IP_AUTODETECTION_METHOD
                  value: "interface=eth1"
    

decoder 介绍

python decoder(装饰器)是python的高级用法,用来拓展函数或类的功能。不过也增加初学者 阅读代码的难度

例如flask可以在需要验证用户登录的请求函数上添加装饰器,只有当用户登录了才可以调用该>页面,非常方便

原理

decoder 其实也是一个函数,装饰器函数会返回真实函数,并会增加额外的代码,如

# 定义wraper
def log(func):
    def wrapper(*arg):
        print('func called')
        return func(*arg)

    return wrapper

# 使用
def hello(Name=None):
    print('Hello %s'%Name)

new_hello = log(hello)

new_hello('bob')
输出 func called Hello bob

使用wraper 的原因

提供额外的功能,例如计时,日志等。 另外wraper还有更多强大的功能

def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)

这种递归调用非常费时,而使用wraper 可以提供一个缓存器

def memory(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memory
def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)


## 给类wraper

上面是函数wraper 类也可以作为wraper 但需要类支持()调用类似c++的类函数
在python里需要实现 `__call__` 方法

```python
import functools

class MemoryClass():
    def __init__(self, is_logging, func):
        self.is_logging = is_logging
        self.func = func
        if self.is_logging:
            print('logging')
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)

        return self.cache[args]

def memory(is_logging):
    return functools.partitial(MemoryClass, is_logging)


@memory(is_loging=True)
def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)


print(fib(5))
类wrapr 可以增加参数,相比函数装饰器,类装饰器具有更加灵活,封装性更好等优点~

wraper 带来的问题

  1. 改变原来的函数属性,例如函数名, 因为装饰器用新函数替换了原来的旧函数。 所以新函>数少了旧函数的属性 解决办法
import functools

def advertising(func):
    @functools.wraps(func)
    def wrapper(*args):
        print('欢迎关注微信公众号: Charles的皮卡丘')
        return func(*args)
    return wrapper

@advertising
def add_1(a, b):
    '''加法运算'''
    return a + b
  1. 获取参数
import inspect
import functools

def check(func):
    a = 0
    @functools.wraps(func)
    def wraper(*args, **kwargs):
        a += 1
        print(a)
        # 在wraper里检查参数
        getcallargs = inspect.getcallargs(func, *args, **kwargs)
        if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
            return False
        return func(*args, **kwargs)

    return wraper


@check
def dowork(arg1, arg2):
    return arg1 + arg2

dowork(1,2)

此时会报错,a在赋值之前引用了。 需要将a声明为 nonlocal

区别

Dockerfile 中,ENTRYPOINT和CMD来都可以用来设置默认命令,而代替docker run时的命令,也可以一起使用

单独使用ENTRYPOINT或CMD时,其区别在于对docker run的参数处理:
CMD的命令会被docker run的参数覆盖,而ENTRYPOINT是追加

例如:

# image1
FROM ubuntu
ENTRYPOINT ["echo", "hello world"]
# image2
FROM ubuntu
CMD ["echo", "hello world"]

执行结果

# docker run image1
hello world

# docker run image1 hostname
0c33ff9af81c

# docker run image2
hello world

# docker run image2 hostname
hello world 0c33ff9af81c

实际情况多是entrypoint与cmd组合使用

cmd 为entrypoint 提供默认参数, 即如果'docker run'给了参数时,覆盖CMD参数,否则使用CMD提供的参数

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

首先说cmd和entrypoint 都有两种模式

  1. shell模式, 即没有用数组来传递参数, 直接接命令如 CMD top -p 或者 entrypoint top -p
  2. exec 模式, 即使用数组表示如 CMD ["top", "-p"], 注意必须用双引号,因为这个是用json来传递的

两者区别:
shell模式不会接受命令行参数,而且1号进程是sh,而非如上的top。 这样的话top不能接受到docker stop container时的信号。 而一般我们写的程序都是捕捉SIGTERM来进行peaceful exit,那么就一定不能使用shell模式

exec模式时, 可以接受docker run image param, 这个param会传递给top作为参数。 而且top 是1号进程,能接收信号。

细微区别: 由于shell模式是先启动sh, 符号解析由shell执行,例如$HOME的展开由shell进行, 而用exec模式时,由docker 进行

总之,推荐使用exec 模式

额外话, 减小镜像体积

使用'\' 或者 '&&' 将命令连起来可以减小镜像层数,从而减小体积。另外build时,单独一行的命令会在新的layer 层执行。有些命令产生的cache,会持续影响到下一层
例如RUN apt-get dist-upgrade -y,如果不想保留cache,需要在build时加入参数docker build --no-cache.

还有最有效减小镜像体积的方式,也常见的是最后用空白镜像,'FROM SCRATCH', 这样把其他的层都干掉了, 只保留一层镜像

问题, 如果在文件系统之上再创一个文件系统?

例如在ext3的文件系统上创建一个xfs的文件系统,可以通过回环设备loop, 我们经常通过 mount -o loop 来 mount一个iso文件 但mount 的选项总是ro的

mount: /mnt: WARNING: device write-protected, mounted read-only.

不仅如此, 先在当前文件系统dd出一个文件, 再绑定到loop设备上,然后mount 到某个目录后, 可以进行读写访问

[root@ha1 ~]# dd if=/dev/urandom of=file bs=1M count=2
2+0 records in
2+0 records out
2097152 bytes (2.1 MB, 2.0 MiB) copied, 0.0164121 s, 128 MB/s

[root@ha1 ~]# mkfs.ext3  file 
mke2fs 1.44.6 (5-Mar-2019)
Discarding device blocks: done                            
Creating filesystem with 2048 1k blocks and 256 inodes

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

[root@ha1 ~]# mount -o loop file  /mnt
[root@ha1 ~]# ls /mnt/
lost+found

[root@ha1 mnt]# echo abc > abc
[root@ha1 mnt]# ls
abc  lost+found
绑定loop设备和挂载是由mount 一个命令完成的。

手动绑定loop

[root@ha1 ~]# losetup -f
/dev/loop1

[root@ha1 ~]# losetup -f file 
这样只是将文件绑定了loop设备,需要再挂载到文件目录mount /dev/loop0 /mnt

[root@ha1 ~]# ls /mnt/
abc  lost+found
[root@ha1 ~]# 

losetup -f 可以返回第一个未被使用的loop设备名

创建loop设备

有的系统默认创建了 loop0 .. loop7 的块设备,有的则是在需要的时候创建,比如mount iso的时候发现没有loop设备,则会创建

  1. 手动创建loop设备通过 mknode 创建
mknode  /dev/loop0 b 7 0
mknode  /dev/loop1 b 7 0
...

mknode /dev/loop7 b 7 0
  1. 如果8个loop0 .. loop8 设备都占用了, 可以再创建loop8

$sudo mknod /dev/loop8 b 7 8 

$ls -l /dev/loop8
brw-r--r-- 1 root root 7, 8 Jun 11 19:16 /dev/loop8
ps: /dev/loop* 是块设备

bash 脚本安全

不管是大厂还是小厂,总能听到一些误操作新闻
那避免误操作就办法就是写脚本, 然后也有很多脚本出事的新闻, 通常有以下

引用了不存在的变量

#!/bin/bash

ABC_PATH=abc

/bin/rm ${ABC_PATH}/

如果ABC_PATH变量被unset了, 那么就是删除系统了
不过听说/bin/rm / 的操作会可能被系统禁止, 但是/bin/rm /*则肯定能执行成功

脚本没有输出

有很多公司用堡垒机来审计操作记录, 但是如果脚本没有输出,那么就不知道执行了什么命令, 再如果脚本也被删除死无对证,那就无法分析了

解决办法

在bash 脚本前面加上 set -xeu

-x 调试模式,bash会输出所有命令 -e 遇到错误就退出 -u 引用了不存在的变量就退出

添加默认值

path=${ABC_PATH-/} #default /

这样,当ABC_PATH不存在时,就使用默认值'/'

最好的办法

使用ansible, 可以对脚本进行版本管理和review, 可以避免脚本难以维护的问题 使用ide + bash 插件, 可以有提示

良好的脚本习惯

除了在脚本前面加上 set -xeu外, 有一些建议

  1. 多用&&,|| ,少用 ; ,如果前面的命令出错,后面就不会执行, 而; 不管
  2. 善用 date, 例如将日志文件加上日期 bash test.sh > log_date +%m_%d_%H_%M_%S``
  3. 脚本放在指定目录,不要与工作目录放在一起, 避免脚本自己被删, 死无对证
  4. 定期备份

bash 的条件判断

有以下脚本,当TMP 不为空的时候,才进行判断值是否大于2

if [ -n "$TMP" -a ${TMP} -gt 2 ];then
        echo "aaaa"
else
        echo "bbbbbbbb"
fi

而当TMP 为空的时候,就会出现 too many arguement。这就说明了bash的执行流程,是先进行整行替换再执行(起码是整行,也许还是全文替换)。 替换后[-n "" -a -gt 2]。导致了报错。 解决这样的问题可以用默认值 TMP=${TMP:=2}, 这样当TMP为空的时候就会自动赋值为2

if [ -n "$TMP" -a ${TMP:=2} -gt 2 ];then
        echo "aaaa"
else
        echo "bbbbbbbb"
fi

register 关键字

register 关键字是c/c++ 里面唯一能操作寄存器的指令,强制将数据存放在寄存器里,而不是放在堆栈里,这样读写速度最快。

而汇编语言强大之处是能直接操作寄存器,且可以指定使用哪些寄存器。

通过汇编一段代码来演示register的作用

先是c语言代码

int main(){

    register int i;

    for(i = 0 ;i<10;i++){
        int j = i;
    }

}
对应的汇编
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        pushq   %rbx
        .cfi_offset 3, -24
        movl    $0, %ebx
        jmp     .L2
.L3:
        movl    %ebx, -12(%rbp)
        addl    $1, %ebx
.L2:
        cmpl    $9, %ebx
        jle     .L3
        movl    $0, %eax
        movq    -8(%rbp), %rbx
        leave
        .cfi_def_cfa 7, 8
        ret

没有register关键字的时候

int main(){

    int i;

    for(i = 0 ;i<10;i++){
        int j = i;
    }

}

main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $0, -8(%rbp)
    jmp .L2
.L3:
    movl    -8(%rbp), %eax
    movl    %eax, -4(%rbp)
    addl    $1, -8(%rbp)
.L2:
    cmpl    $9, -8(%rbp)
    jle .L3
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret

可见,当register修饰int i时, i的值存放在%ebx里,当没有register修饰时,i的值放在栈里%rbp -8的位置 movl %ebx, -12(%rbp) 可见当register时, j的值复用了ebx的高32位。 此时的内存如下

--- 栈底
旧的rbp 64位
---
旧的rbx 高32位
---  j
旧的rbx 低32位
---栈顶

rbp表示基地址指针,指向的是函数栈的起始地址,这个起始地址只是对于当前函数而言的
rsp表示栈顶指针,指向当前的栈顶
每次函数调用时(main 函数被内核_start_main__调用): 1. 将调用者的函数栈基地址保存起来
2. 参数压栈, 可能由调用者压栈,可能由被调用者压栈,影响的是当前的栈顶,继而栈顶影响rbp 3. 将被调用函数当前的栈顶复制给基地址寄存器。新的rbp == rsp即当前的栈顶。相当于构造了新的函数运行环境

rbp和rsp

实例代码,main函数中调用foo函数。

int foo(){
  printf("hello");
  return 1;
}

int main(){
  foo();
  return 0;
}

(gdb) disassemble 
Dump of assembler code for function main:
   0x0000555555555158 <+0>: push   %rbp
   0x0000555555555159 <+1>: mov    %rsp,%rbp
   0x000055555555515c <+4>: mov    $0x0,%eax
=> 0x0000555555555161 <+9>: call   0x555555555139 <foo>
   0x0000555555555166 <+14>:    mov    $0x0,%eax
   0x000055555555516b <+19>:    pop    %rbp
   0x000055555555516c <+20>:    ret
  1. main 函数> push %rbp 将栈底寄存器的值保存到堆栈中
  2. main 函数> mov %rsp,%rbp 将当前的栈顶寄存器的值复制到栈底寄存器
  3. main 函数> mov $0x0,%eax 将foo函数的返回值初始化成0

进入到foo函数中

(gdb) disassemble 
Dump of assembler code for function foo:
   0x0000555555555139 <+0>: push   %rbp
   0x000055555555513a <+1>: mov    %rsp,%rbp
=> 0x000055555555513d <+4>: lea    0xec0(%rip),%rax        # 0x555555556004
   0x0000555555555144 <+11>:    mov    %rax,%rdi
   0x0000555555555147 <+14>:    mov    $0x0,%eax
   0x000055555555514c <+19>:    call   0x555555555030 <printf@plt>
   0x0000555555555151 <+24>:    mov    $0x1,%eax
   0x0000555555555156 <+29>:    pop    %rbp
   0x0000555555555157 <+30>:    ret
End of assembler dump.

附录

General-Purpose Registers

The 64-bit versions of the 'original' x86 registers are named:

    rax - register a extended
    rbx - register b extended
    rcx - register c extended
    rdx - register d extended
    rbp - register base pointer (start of stack)
    rsp - register stack pointer (current location in stack, growing downwards)
    rsi - register source index (source for data copies)
    rdi - register destination index (destination for data copies)

The registers added for 64-bit mode are named:

    r8 - register 8
    r9 - register 9
    r10 - register 10
    r11 - register 11
    r12 - register 12
    r13 - register 13
    r14 - register 14
    r15 - register 15

These may be accessed as:

    64-bit registers using the 'r' prefix: rax, r15
    32-bit registers using the 'e' prefix (original registers: e_x) or 'd' suffix (added registers: r__d): eax, r15d
    16-bit registers using no prefix (original registers: _x) or a 'w' suffix (added registers: r__w): ax, r15w
    8-bit registers using 'h' ("high byte" of 16 bits) suffix (original registers - bits 8-15: _h): ah, bh
    8-bit registers using 'l' ("low byte" of 16 bits) suffix (original registers - bits 0-7: _l) or 'b' suffix (added registers: r__b): al, bl, r15b

Usage during syscall/function call:

    First six arguments are in rdi, rsi, rdx, rcx, r8d, r9d; remaining arguments are on the stack.
    For syscalls, the syscall number is in rax.
    Return value is in rax.
    The called routine is expected to preserve rsp,rbp, rbx, r12, r13, r14, and r15 but may trample any other registers.

参考

https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html

乐观锁悲观锁

先说这两种锁都不是编程里面用到的锁, 而是一种策略。 在数据库中用的多。
而且在java里面有个原子bool AtomicBoolean 的实现也用到乐观锁的策略, AtomicBoolean::compareandswap

这里以数据库管理服务(DBMS)来说明这两种锁的区别,在并发的情况下

悲观锁

悲观锁策略预期是多个请求会操作同一个数据,导致数据不可靠。 悲观锁的策略就是上锁,不让其它请求修改, 即排他锁。

这种排他锁与我们平常编程用的互斥锁道理相同, 是性能比较低的一种锁,但能保证数据准确。

乐观锁

乐观锁实际是不上锁, 预期多个请求不会修改同一分数据。而是在最后提交的时候再去确认数据是否一致,如果一致则提交,如果不一致则报错,需要认为干涉

所以乐观锁的数据也是可靠的, 且性能较好。 这里关键是如何确认数据一致

乐观锁的确认并提交

这里举例sql的来说明

select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity=3
更新时,会确认quantity为预期的3,否则不执行或者报错。

这样有个问题, 虽然单条语句是原子的,但是两条语句不是, 会出现ABA的情况

例如当一个请求1执行到select后,得知quantity=3, 然后请求2先更新quantity=2,然后又更新为quantity=3 然后请求1继续执行update,这会成功,虽然quantity=3是一致的,但是不能保证其它数据是一致的或者完全没有引起安全问题

因此另一种办法是判断只增version或者timestamp,每次请求时去更新。这两种办法都能确保数据一致。 但是因为数据库同时只允许一个请求修改version,那么就出现了竞争情况,会导致其它请求失败

例如多个请求同时记录了version=1, 则当一个请求完成的时候,version=2, 那么其它请求提交时发现verison变了,只能报告失败 虽然这样能保证数据正确,但是会导致其它请求失败

而电商平台中, 则采用另一种方式确认

select quanity from items where id =1;

//得到quanity=3,然后更新

update items set quantity=2 where id=2 and quantity > 0

这样就规避了上面两个方法的缺点

compareandswap

compareAndSet(expect_val, new_val)

上面的乐观锁就是compareandswap的解释:

先对比,如果一致则更新

再说一下java的实现,

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))  
      UnsafeWrapper("Unsafe_CompareAndSwapInt");  
      oop p = JNIHandles::resolve(obj);  
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);  
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;  
    UNSAFE_END  

在unsafe.cpp 找到实现如下

#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:

int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
_emit 是用于生成指令的, 生成锁的指令,有cpu提供锁机制 从上下文看, 这个指令应该是上锁, 如果mp != 0 则上锁。