python:3.6.5
Django:2.1.1
Project:Kubernetes
,文件夹路径就是/django/Kubernetes/
App:createyaml
,文件夹路径就是/django/Kubernetes/createyaml
前文地址:https://rorschachchan.github.io/2018/09/26/Django%E4%BD%BF%E7%94%A8form%E8%A1%A8%E5%8D%95%E5%88%A4%E6%96%AD%E8%BE%93%E5%85%A5%E5%80%BC%E6%98%AF%E5%90%A6%E5%90%88%E6%B3%95/
之前我们已经达到了“页面判断输入值是否合法”,“页面输入值录入数据库”这两个目的,现在就到了重头戏–网页上点击按钮,然后调用后台python脚本,并且把脚本的结果反馈到网页端。
我们本次使用一个加密的python脚本encrypt.py
,它主要得作用是输入某个字段,然后进行AES256加密,然后把加密结果返回给界面,整个脚本内容如下:
1
2
3
4
5
6
7
#coding=utf-8
import subprocess
AESWord = input("输入字段:")
result = list(subprocess.getstatusoutput("java -jar /yunwei/AES/aesEncrpt.jar "+AESWord))[1].split("=")[1]
print (AESWord+ "的加密结果是:"+(result))
脚本执行效果如下:
前端的页面内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13{% extends 'base.html' %}#这部分是引入base.html这个模板
{% block title %}
AES加密
{% endblock %}
{% block content %}
<form action="/k8s/encrypt/" method="post" name='encrypt'>
{% csrf_token %}
要加密的字段:<input type="text" name="AESWord" /><br />
<input type="reset" value="清除所有" />
<input type="submit" value="查询解析" />
</form>
{% endblock %}
目前已知views.py
里使用request.POST.get()
方法是可以捕获到前端输入值,但是这个输入值怎么传递给encrypt.py
呢?这一点非常的复杂。
可能这个时候很多人会想使用“外部脚本引入django系统”的方法,但是那个方法可以引用到数据库,但是无法引用views.py
里的函数的变量。于是只能用一个笨招:先把前端输入值记录到本地某个文件里,然后encrypt.py
去读取这个文件,这样达到获取变量的方法。
于是views.py
里的相关部分就是这样:
1
2
3
4
5
6
7
8
9
10
11def encrypt(request):
if request.method == 'POST':
AESWord = request.POST.get('AESWord')
with open('/yunwei/AES/AESWord.txt','w') as f:#把前端获取到的值记录到本地的AESWord.txt文件里
f.write(AESWord+"\n")
child = subprocess.Popen('python /yunwei/AES/Encrypt.py',stdout=subprocess.PIPE, stderr=subprocess.PIPE,shell=True)
stdout, stderr = child.communicate()
result = str(stdout,encoding='utf-8')#将脚本反馈的结果输入result
return HttpResponse(result)#页面展示result
else:
return render(request,'encrypt.html')
而encrypt.py
内容改成如下:
1
2
3
4
5
6
7#!/usr/bin/env python
#coding=utf-8
import linecache,subprocess
AESWord = linecache.getline('/yunwei/AES/AESWord.txt',1).strip('\n')#在这里读取前端的变量
result = list(subprocess.getstatusoutput("java -jar /yunwei/AES/aesEncrpt.jar "+AESWord))[1].split("=")[1]
print (AESWord+ "的加密结果是:"+(result))
执行效果如下:
这样的操作达到了目的!后期就是把result
使用render
加工映射到某个网页,页面就好看很多了。
上面的方法虽然可以达到我们想要的目的,但是其实是十分不推荐的:一是因为网页调用本地程序的权限正在被取消,二是因为真不如JS写直接,三是只能在自己本地调用。
所以还是用前端来解决更专业更优雅,那么就要使用js+ajax。
具体内容下次补充…
在外部脚本引入django系统的方法就是在外部脚本的开头加上下面的内容:
1
2
3
4
5
6
7
8
#coding=utf-8
import os,sys,django
sys.path.append('/django/Kubernetes/') # 将项目路径添加到系统搜寻路径当中
os.environ['DJANGO_SETTINGS_MODULE'] = 'Kubernetes.settings' # 设置项目的配置文件
django.setup()
from createyaml.models import parameter#这样就可以引入models.py文件里的parameter这个类
但是上面说过,这个方法可以引入数据库models.py
文件,并不能引入views.py
文件。
https://stackoverflow.com/questions/15151133/execute-a-python-script-on-button-click
https://blog.csdn.net/yzy_1996/article/details/80223053
https://simpleisbetterthancomplex.com/tutorial/2016/08/29/how-to-work-with-ajax-request-with-django.html
https://www.candypapi.com/2017/11/02/Python-external-script-calls-the-Django-project-model-table/
https://segmentfault.com/q/1010000005096919
Git clone的时候可能会出现fatal: HTTP request failed
的错误,如图:
一般来说这样的情况多半就是git版本太低,<=1.7的版本经常出现这样的错误,解决问题的办法就是使用最新的git,安装git 1.9的方法在这里:https://rorschachchan.github.io/2018/06/13/Centos6%E5%AE%89%E8%A3%85git1-9%E5%AE%89%E8%A3%85%E8%BF%87%E7%A8%8B/ 。
更新到1.9之后重新去git clone,这一次换成了SSL connect error
错误:
此时就需要执行一下yum update -y nss curl libcurl
,这样才能顺利的git clone。
如果出现了easy_install command not found
,可以使用wget https://bootstrap.pypa.io/ez_setup.py -O - | python
解决,有了easy_install就可以安装pip了。
以上的操作是在python2.7下进行的。
]]>数据可视化肯定需要前端知识,同时也要美化前端,让用户的体验更好,这时候就需要接触到css技术。
css简单来说就是先给你需要修饰的部分设定变量,然后针对不同的变量做不同的声明,达到修改界面的目的。css规则由两个主要的部分构成:选择器,以及一条或多条声明,格式是:selector {declaration1; declaration2; ... declarationN }
。
在html文本里添加一个style标签,比如:<style type="test/css"> </style>
。这个标签可以放到<body>
最尾处也可以放到<head>
最尾处。不过一般来说都是放到<body>
里。
在调整css的时候,可以搭配chrome的F12键直接修改,然后将修改的内容拷贝粘贴到html文件里。
比如我现在的页面是如下这个样子的:
这个结构可以看出使用最直白的html语言编写,为了美观大方,我们需要把它改成如下的样子:
原来的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13<body>
<div>
<a href="{% url 'home' %}">
<h2>Homepage</h2>
</a>
<a href="{% url 'blog_list' %}">List</a>
<a href="http://www.baidu.com">跳往百度</a>
<a href="http://www.lechange.com">跳往乐橙</a>
</div>
<hr>
{% block content %} {% endblock %}
</body>
</html>
更改后的代码如下:
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
32
33
34
35
36
37
38
39
40
41
42<body>
<div class="nav">
<!-- 给这个div标签添加一个class叫nav -->
<a class="logo" href="{% url 'home' %}">
<!-- 给这个div下的这个a标签添加一个class叫logo -->
<h2>Homepage</h2>
</a>
<a href="{% url 'blog_list' %}">List</a>
<a href="http://www.baidu.com">跳往百度</a>
<a href="http://www.lechange.com">跳往乐橙</a>
</div>
<hr>
{% block content %} {% endblock %}
<style type='text/css'>
body{
margin: 0;
padding: 0;
<!-- 这是对整个body标签进行声明,外边距和内边距都是0 -->
}
div.nav{
background-color: #eee;
border-bottom: 2px solid blue;
padding: 5px 10px;
<!-- 这是对整个nav的div标签进行声明:颜色灰色 -->
<!-- 增加一条底线取代<hr>,设定宽是2px,实线,颜色是蓝色 -->
<!-- 设定上下边距5px,左右边距10px -->
}
div.nav a{
text-decoration: none;
color: #000;
<!-- 这是对整个nav的div标签里的所有a标签说明:取消下划线,并且规定为黑色 -->
}
div.nav a.logo {
display: inline-block;
color: green;
font-size:120%;
<!-- 在这里对nav的div标签里那个叫logo的a标签进行单独的说明:缩进,并且规定为绿色 -->
<!-- 字体大小是原来的120% -->
}
</style>
</body>
</html>
调整css是一个很繁琐很麻烦的事情,需要耐心。至于如何整合css样式到一个文件然后统一配置的内容,请去看:https://rorschachchan.github.io/2018/05/12/%E5%8A%A0%E8%BD%BDcss%E6%A0%B7%E5%BC%8F%E7%9A%84%E4%B8%A4%E4%B8%AA%E6%96%B9%E6%B3%95/ 。
]]>在编写django的时候,前端html文件里经常会遇到很多有大量重复代码的情况出现,为了代码精简好看以及后期维护的方便,就需要把那些重复的代码统一放到一个文件里去,不重复的部分自然保留,文件到时直接调用重复模板就好,不同的部分对应填充。
举个例子,有一个代码是templates/aaa.html
:
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<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>{{ blog.title }}</title><!-- blog.title就是文章标题,从数据库中提取,使用vender映射出来 -->
</head>
<body>
<div>
<a href="{% url 'home' %}">
<h2>BACK TO HOMEPAGE</h2><!-- 这部分是重复的 -->
</a>
</div>
<h3>{{ blog.title }}</h3><!-- 这一部分也是同样用vender映射,展现每一篇文章对应的作者和内容 -->
<p>作者:{{ blog.author }}</p>
<p>分类:
<a href="{% url 'blogs_with_type' blog.blog_type.pk %}">
{{ blog.blog_type }}
</a>
</p>
<p> {{ blog.blog_type.pk }}</p>
<p>发表时间:{{ blog.created_time|date:"Y-m-d H:i:s"}}</p> <!-- 这里规定了时间格式 -->
<hr>
<p>{{ blog.content }}</p>
</body>
</html>
假设aaa.html
里”BACK TO HOMEPAGE”这个部分是重复的,即每一个页面都有返回主页的点击。既然都有这个功能,那么就单独做一个base.html
文件当框架,把重复的部分写进去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>{% block title %}{% endblock %}</title><!--这里加入了一个block(块),块的名字叫title-->
</head>
<body>
<div>
<a href="{% url 'home' %}">
<h2>BACK TO HOMEPAGE</h2>
</a>
</div>
<hr>
{% block content%} {% endblock %}<!--这里又加入了一个block,块的名字叫content-->
</body>
</html>
现在的base.html
就是一个框架,里面有了两个block,这两个块有各自的名称,因为这两个块的内容是变化的。再把aaa.html
里需要对应配置的部分定义成对应的变量,并且引入这个base.html
即可。重新修理后的aaa.html
就长这个样子了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{% extends 'base.html' %}<!--首先引入同目录下的base.html-->
{% block title%}
{{ blog.title }}<!--这部分就是title块的内容-->
{% endblock%}
{% block content %} <!--这一段就是content块的内容-->
<h3>{{ blog.title }}</h3>
<p>作者:{{ blog.author }}</p>
<p>分类:
<a href="{% url 'blogs_with_type' blog.blog_type.pk %}">
{{ blog.blog_type }}
</a>
</p>
<p> {{ blog.blog_type.pk }}</p>
<p>发表时间:{{ blog.created_time|date:"Y-m-d H:i:s"}}</p>
<hr>
<p>{{ blog.content }}</p>
{% endblock %}
将aaa.html
保存之后,刷新对应的页面,会发现依旧可以成功读取而且界面没有任何的变化。
可是在实际操作中也会出现这样的需求:多个不同的django APP可能会要访问同一个模板文件(即base.html),那么就要每一个app都复制一遍base.html吗?其实大可不必。这里可以修改一下setting.py
,在里面设置一下公共的模板文件路径。
首先我们现在project根目录下建立一个base文件夹,把base.html
复制进去,然后修改一下setting.py
如下的字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR,'base'),#BASE_DIR是在文件最开始定义的,即project的根目录
],
'APP_DIRS': True,#这句话的意思是templates文件夹里所有的文件都可以访问
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
保存之后,再次刷新界面,发现界面没变化。这里django在寻找页面的时候,就会去project的路径/base下先找对应的文件,如果没有,会再去自己应用下的templates文件夹里找。如果两个都没有,那就会报错base.html is not exist
。
Nginx配置IP白名单是非常基础的工作,这次试验就是配置某网页可以正常被部分IP访问,而其他网页访问将是403。目标网页地址是http://xxdtq.lechange.com/test/test.html
,内容如下:
本机的外网IP地址是115.205.2.28
,如图:
首先先nginx.conf
里的日志配置格式如下:
1
2
3log_format access '$http_x_forwarded_for - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $remote_addr $request_time $upstream_response_time $http_host';
Nginx的转发文件default.conf
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15server {
listen 80;
server_name xxdtq.lechange.com;#如果浏览器输入的是xxdtq.lechange.com,那么就跳转到82端口
location / {
proxy_pass http://localhost:82;
}
}
server {
listen 80;
server_name xhssf.lechange.com;#如果浏览器输入的是xhssf.lechange.com,那么就跳转到82端口
location / {
proxy_pass http://localhost:83;
}
}
现在配置xxdtq.conf
文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21server
{
listen 82 default;#82端口
server_name xxdtq.lechange.com;
root /mnt/xiuxiudetiequan/;#根目录是/mnet/xiuxiudetiequan/
index index.html index.htm index.php;
add_header Set-Cookie "HttpOnly";
add_header Set-Cookie "Secure";
add_header X-Frame-Options "SAMEORIGIN";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location = /test/test.html {#如果remote_addr是125.205.2.28来访问/test/test.html,那么就返回403
if ($remote_addr = 115.205.2.28) {
return 403;
}
}
access_log /var/log/nginx/xxdtq/access.log access;
error_log /var/log/nginx/xxdtq/error.log error;
}
执行了nginx -s reload
后,刷新一下界面,却发现页面没变,并不是预期中的403,打开nginx的日志一看,发现获取到的$remote_addr
是127.0.0.1
!如下:
为什么是127.0.0.1?因为我们这个nginx做了一个80到82端口的转发呀,所以到80的地址是真实的外界IP,而80转发到82就是本机IP了。那这样的情况怎么办?就需要在default.conf
里添加一句proxy_set_header x-forwarded-for $remote_addr;
,如下:
1
2
3
4
5
6
7
8server {
listen 80;
server_name xxdtq.lechange.com;
location / {
proxy_pass http://localhost:82;
proxy_set_header x-forwarded-for $remote_addr;
}
}
重启一波nginx,发现http_x_forwarded_for
正是远程访问的IP地址115.205.2.28
,于是将xxdtq.conf判断IP改成如下内容:
1
2
3
4
5location = /test/test.html {
if ($http_x_forwarded_for = 115.205.2.28) {#改用http_x_forwarded_for
return 403;
}
}
重启nginx之后,果然页面是403,如图:
然后用其他的IP地址,比如用手机连接4G去打开http://xxdtq.lechange.com/test/test.html ,发现是正常读取的,试验成功!
如果是要整个/test/目录都不让访问的话,就要改成如下内容:
1
2
3
4
5location ^~ /test/ {
if ($http_x_forwarded_for = 115.205.2.28) {# =是精确匹配
return 403;
}
}
如果要配置多个IP地址,就要改成如下内容:
1
2
3
4
5location ~ ^/shopadmin {
if ($remote_addr ~* "第一个IP|第二个IP|第三个IP") {#这里改成~*
return 403;
}
}
nginx日志中的http_x_forwarded_for
字段会有多个IP。使用自定义的模板,grok
常用表达式的IPORHOST匹配http_x_forwarded_for
该字段,获取的IP值是最后一个,如何取第一个IP值?
答案是:
1
2
3
4mutate {
split => ["http_x_forwarded_for",","]
add_field => ["real_remote_addr","%{http_x_forwarded_for[0]}"]
}
IPORHOST这些变量匹配不到所有IP,只能通过自定义正则来匹配到所有IP;再通过以上方法截取第一个IP值。正则表达式写法是:[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\,\s]*
http://seanlook.com/2015/05/17/nginx-location-rewrite/
https://zhuanlan.zhihu.com/p/21354318
http://blog.pengqi.me/2013/04/20/remote-addr-and-x-forwarded-for/
http://gong1208.iteye.com/blog/1559835
https://my.oschina.net/moooofly/blog/295853
在监控zookeeper的时候,我写了一个简单的脚本checkZKrole.sh
来获取当前的角色,如下:
1
2
3
4[root@zookeeper1 ~]# cat checkZKrole.sh
#!/bin/bash
role=$(sh /usr/zookeeper/bin/zkServer.sh status| cut -d" " -f2)
echo $role
执行效果如下:
本地执行没问题,然后在zabbix-agentd.conf
里也把这个脚本添加到自定义监控项里:
重启了zabbix-agent
后,发现一个很奇怪的现象,在zabbix-server里使用zabbix-get
去拿值的时候拿到的是contacting
,如图:
从上图可见,同样在127.1.1.28里取值,proc.num
没问题,而且是秒取,但是这个自定义项就取不到。
我怀疑是脚本的问题,于是我改成一个单纯的echo
,如下:
1
2
3
4
5[root@zookeeper1 ~]# cat checkZKrole.sh
#!/bin/bash
role=$(sh /usr/zookeeper/bin/zkServer.sh status| cut -d" " -f2)
#echo $role
echo woshinibaba
这一次的返回值是正常的,可见不是脚本的问题:
那是他妈的什么问题,真是见了鬼了…后来想干脆写一个crontab,让crontab把角色写到本地,然后再用cut命令切开把结果当做zabbix_get的目标。但是在这里发现了问题所在,当我的crontab是* * * * * cd /usr/zookeeper/bin/; ./zkServer.sh status > /tmp/role.txt > /dev/null 2>&1
,发现/tmp/role.txt
里根本没有值,应该是crontab在执行有参数的命令的时候出现了问题。
后来发现了,原来是sudo搞得鬼,如果是由于zookeeper是root用户启动的,所以只有root用户能成功访问,如果是sudo的话,那么就会返回“Error contacting service. It is probably not running.”,所以截取出来的部分就是contacting,如图:
在监控mq队列时候,同样也需要到了自定义监控项,我写了几个简单的脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17[root@dahuatech zabbix]# cat monitor_mq.sh
#!/bin/sh
ip=$1
queuename=$2
type=$3
case ${type} in
Pending)
curl -s -u 'admin:admin' "http://${ip}:8161/admin/queues.jsp"|grep "${queuename}</a></td>" -A 5|sed -n '2p'|egrep -o '[0-9]+'
;;
Enqueued)
curl -s -u 'admin:admin' "http://${ip}:8161/admin/queues.jsp"|grep "${queuename}</a></td>" -A 5|sed -n '4p'|egrep -o '[0-9]+'
;;
Dequeued)
curl -s -u 'admin:admin' "http://${ip}:8161/admin/queues.jsp"|grep "${queuename}</a></td>" -A 5|sed -n '5p'|egrep -o '[0-9]+'
;;
esac
配置了UserParameter=activemq.check[*],sh /etc/zabbix/monitor_mq.sh $1 $2 $3
放到zabbix-agentd.conf
里,重启了zabbix-agent。在zabbix-server配置了对应的item,如图:
然后在本地执行这个脚本,发现回值秒取,但是同样在zabbix-get里使用,就是timeout:
后来发现原来自己摆了一个乌龙,在zabbix-get的时候不能使用{HOST.IP}
,因为zabbix-get
不识别他,但是zabbix-server
是识别的,所以在脚本里把ip=$1
改成ip=真实的IP地址即可。
主从同步是Mysql非常常见的一个应用,也是非常重要的监控之处,这里简单总结在配置Mysql时候的几个要点,防止以后自己踩坑。
先说一下主从同步的原理,就是主数据库在数据库更新的时候会更新自己的binlog
,同时也会向读数据库(一个或多个)传递这个binlog
,此时从库开始一个io_thread
这个线程用来接收这个binlog
,然后把binlog写入到自己的relaylog
,当relaylog
发现有数据更新了,就开始一个sql_thread
来按照主库更新自己的库,这样达到了“主库读库一致”的效果。图示如下:
上述过程:
主从延迟:「步骤2」开始,到「步骤7」执行结束。
步骤 2:存储引擎处理,时间极短
步骤 3:文件更新通知,磁盘读取延迟
步骤 4:Bin Log 文件更新的传输延迟,单线程
步骤 5:磁盘写入延迟
步骤 6:文件更新通知,磁盘读取延迟
步骤 7:SQL 执行时长
要监控主从同步是否出现异常,可以通过show slave status\G
里的Seconds_Behind_Master
字段来查看,如图:
但是要注意!Seconds_Behind_Master
是有前提的,那就是主库跟读库之间的网络情况要良好,因为这个字段是从属服务器SQL线程和从属服务器I/O线程之间的时间差距,(即比较binlog
和relaylog
执行sql的timestamp时间差),单位是秒。如果主服务器和从属服务器之间的网络连接较快,则从属服务器I/O线程会非常接近主服务器,所以本字段能够十分近似地指示,从属服务器SQL线程比主服务器落后多少。如果网络较慢,则这种指示不准确;从属SQL线程经常会赶上读取速度较慢地从属服务器I/O线程,因此,Seconds_Behind_Master
经常显示值为0。即使I/O线程落后于主服务器时,也是如此。换句话说,本列只对速度快的网络有用。
binlog
文件在生产系统中不易过大,建议小于500m,不然容易拖慢数据库性能;binlog-do-db
参数对应一个库,不能一行写多个库;Slave_IO_Running: No
这个状态,去主库上show master status\G
,查看一下是否file跟从库的file是不是对不上;insert … select … where not exist
这种方式;https://dba.stackexchange.com/questions/24793/mysql-replication-slave-is-continuously-lagging-behind-master
http://ningg.top/inside-mysql-master-slave-delay/
http://database.51cto.com/art/201108/287653.htm
https://zhuanlan.zhihu.com/p/28554242
之前在https://rorschachchan.github.io/2018/05/17/%E4%BD%BF%E7%94%A8zabbix%E5%8E%BB%E7%9B%91%E6%8E%A7docker%E5%AE%B9%E5%99%A8/ 介绍了如何使用dockbix去自动监控容器的cpu、mem和端口等值。而本文的内容就是讲述如何使用dockbix监控进程。
服务器情况如下:
172.31.0.77,普通模式安装zabbix-server;
172.16.0.194,服务器里有两个容器,一个是dockbix,另一个是具体的服务,里面是一个centos 7跑着nginx和php两个进程,如图:
如果你启动dockbix的语句是这样的话:
1
2
3
4
5
6
7
8
9
10docker run \
--name=dockbix-agent-xxl \
--net=host \
--privileged \
-v /:/rootfs \
-v /var/run:/var/run \
--restart unless-stopped \
-e "ZA_Server=zabbix-server的IP地址" \
-e "ZA_ServerActive=zabbix-server的IP地址" \
-d monitoringartist/dockbix-agent-xxl-limited:latest
那么发现监控进程是失败的,如图:
原因就是dockbix和具体服务之间是两个独立的进程,所以dockbix无法访问到另一个容器的进程情况,这样就要干掉原有的dockbix,并且更改一下dockbix的启动语句:
1
2
3
4
5
6
7
8
9
10
11docker run \
--name=dockbix-agent-xxl \
--net=host \
--pid=host \#增加这句话
--privileged \
-v /:/rootfs \
-v /var/run:/var/run \
--restart unless-stopped \
-e "ZA_Server=172.31.0.77" \
-e "ZA_ServerActive=172.31.0.77" \
-d monitoringartist/dockbix-agent-xxl-limited:latest
然后再去重新使用zabbix-get
命令,就可以获取到进程了!
默认下,所有的容器都启用了PID命名空间。PID命名空间提供了进程的分离。PID命名空间删除系统进程视图,允许进程ID可重用,包括pid 1。docker run
的时候添加了--pid=host
就是允许容器内的进程可以查看主机的所有进程。
如果是不要看所有主机的进程,而只是看某一个容器的进程,其他进程pid不看怎么设置呢?
1
2docker run --name my-redis -d redis#假设你启动了一个名叫my-redis的容器
docker run -it --pid=container:my-redis my_strace_docker_image bash#在建立一个my_strace_docker_imag容器,只与my-redis共享pid
如果zabbix-server发现容器内的某个服务死了,要进入容器里重启服务怎么办?答曰:docker exec 容器ID /bin/bash -c "启动服务命令"
https://github.com/monitoringartist/dockbix-agent-xxl/issues/42
https://www.zabbix.com/documentation/3.4/zh/manual/appendix/items/proc_mem_num_notes
https://docs.docker.com/engine/reference/run/#imagetag
今天在调整jumpserver堡垒机资产用户的时候,在点击“更新”的时候,爆出127.0.0.1:3306无法被访问,于是登录到服务器里一看,发现mysql进程挂了。先检查服务器存储空间,发现还很富裕,于是就启动mysql,爆出来如下错误:
1
2[root@lcshop-jumpserver ~]# mysql
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (111)
然后来到/var/lib/mysql/
里,瞅着这个紫了吧唧的mysql.sock
,脑子一抽,把它删了…
删了…
这尼玛,再次启动mysql,错误码从111变成2:
1
2[root@lcshop-jumpserver mysql]# mysql
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)
这一下就尴尬了,mysql.sock
没了怎么生成?有人说“重启服务器可以生成”,事实证明这就是纯粹的扯淡。真实的方法是:mysqld_safe &
。
如果mysqld_safe &
命令失败了,就要去查看一下mysql的日志,多半是某个文件权限不对,要改成mysql用户。
补充一句其他的问题:ImportError: libxslt.so.0: cannot open shared object file: No such file or directory
,遇到这个问题怎么办? yum install libxslt-devel -y
公司的电商平台使用的是阿里云VPC网络,整个交换机和云服务器都是部署在D区。今天在部署测试环境的时候,发现无法购买服务器,在钉钉上与阿里云售后交涉后,接到噩耗—D区已经不再出售服务器了,如图:
没办法,只能把现有的服务器调高配置,在里面安装docker,尽可能的让各进程的环境彼此之间不受干扰。由于事发仓促,整个架构都要重新调整,镜像就先选用centos:latest
,生成容器后在里面装环境以及git pull
代码,把容器当做虚拟机来用了。
如果要pecl install swoole
的话,要先yum install -y glibc-headers gcc-c++ kernel-headers gcc openssl pcre-devel
和yum install -y openssl-devel
;
centos:latest
镜像目前是7.5版本,如果要查看的话需要先安装lsb命令:yum install redhat-lsb -y
;
如果容器里使用yum下载爆’Operation too slow. Less than 1000 bytes/sec transferred the last 30 seconds’,用yum -y install wget
解决;
容器需要php7.2的环境的话,就要用最新的源:
1 | yum install epel-release -y |
别忘了开机自启动docker进程:systemctl enable docker
;
yum install node npm
之前要
1 | yum install -y epel-release |
在容器里查看端口情况就要安装netstat命令:yum install -y net-tools
;
将一个运行中的容器做成镜像的命令:docker commit 容器ID号 镜像名称
;
进入容器最好不要用docker attach 容器ID
的方式,而是用docker exec -it 容器ID /bin/bash
,离开容器的时候也不要用exit
或者ctrl + D
,这样会将容器停止,而是用ctrl + P
、ctrl + Q
或者ctrl + Q + P
组合键退出,这样就不会终止容器运行;
容器默认的时间与宿主机的时间相差8个小时,可以在docker run
的时候使用-v挂载的方法挂载宿主机的时间文件,比如:docker run --name 容器名 -v /etc/localtime:/etc/localtime:ro ...
,或者在dockerfile里添加“设定时区”的语句:
1 | #设置时区 |
容器映射默认情况下是tcp6的,这是正常的现象,如果telnet不通,请先去检查容器内的服务是否正常,比如在容器里curl 127.0.0.1 端口号
;
使用docker top 容器id
命令能获取的PID是容器内进程在宿主机上的pid,ppid是容器内进程在宿主机上的父进程pid;
如果多个容器要挂载一样的数据就是用-volumes-from
,比如docker run --volume-from 容器ID号
;
在容器外启动容器内部进程的方法是:docker exec 容器ID /bin/bash -c "对应的命令"
,在zabbix监控docker发现进程死了后,就可以用这个方法拉起来;
接上一条的说,docker跟虚拟机不同,它启动的时候是不会运行/etc/rc.d/rc.local
的,如果想要Docker在启动后就自动运行/etc/rc.d/rc.local
,请看https://github.com/johnnian/Blog/issues/13 里面说的方法;
容器内的进程是会映射到宿主机上的,举个例子,比如容器里运行了swoole,如图:
在宿主机上看也是能看到这个进程的:
http://blog.sina.com.cn/s/blog_5ff8e0a00102wmti.html
https://outmanzzq.github.io/2018/01/11/docker-exit-without-stop/
http://dockone.io/article/128
https://blog.csdn.net/halcyonbaby/article/details/46884605
https://stackoverflow.com/questions/30960686/difference-between-docker-attach-and-docker-exec
https://www.binss.me/blog/learn-docker-with-me-about-volume/
开发同学反馈某一个开发环境的机器卡的要命,我登录一看,内存已经被耗的差不多,但是一看top又看不出来哪个进程占用了很多的内存,如图:
换ps -e -o 'pid,comm,args,pcpu,rsz,vsz,stime,user,uid' | sort -nrk5
看也没看出来个之乎者也。
发现这个服务器里有两个容器,但是很奇怪,用docker stats却无法获得他们的基础值:
明明容器都是up状态啊,于是我就尝试链接到其中一台,发现报错:rpc error: code = 14 desc = grpc: the connection is unavailable
,而且不能restart和kill,如图:
使用docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc --debug
,发现里面是这样:
后来在https://github.com/moby/moby/issues/30984 这个文章下面找到了一个跟我情况差不多的哥们,也是docker stats
命令失效。解决方法是重启docker进程:systemctl restart docker.service
,果然,重启之后在手动启动上面两个容器,容器就可以正常访问了:
服务器的内存情况也得到了一定的缓解:
后来跟开发复盘,原来是这个机器上一次死机了,没法关闭容器,只能直接在阿里云控制台重启,而正常的流程应该是先关闭容器再重启的。
python:3.6.5
Django:2.1.1
Project:Kubernetes
,文件夹路径就是/django/Kubernetes/
App:createyaml
,文件夹路径就是/django/Kubernetes/createyaml
前文地址:https://rorschachchan.github.io/2018/09/18/Django%E9%80%9A%E8%BF%87%E5%94%AF%E4%B8%80%E6%A0%87%E8%AF%86%E7%AC%A6%E5%B0%86%E5%90%8E%E5%8F%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AF%B9%E5%BA%94%E8%BE%93%E5%87%BA/
对于表单而言,检查用户输入的信息是否合法是必然项。检查合法一般来说都是用JavaScript
或JQuery
。不过我是一个前端白痴,JavaScript
对我来说就是天书。但是Django非常的贴心,在form表单里就准备了“验证输入内容”这个功能。
如果使用这个功能,首先先在app的views.py
里导入form模块:from django import forms
。
导入模块之后,设定一个类,这个类就是要在前端html页面中生成form表单中的input
标签的,比如:
1
2
3
4
5
6class YamlInfo(forms.Form): #定义的django表单
name = forms.CharField(error_messages={'required': u'此节点不能为空'},)#自定义错误信息
replicas = forms.DecimalField(max_digits=2,error_messages={'required': u'副本个数不能大于100'}) #最大只有2位数
labels_app = forms.CharField(error_messages={'required': u'此节点不能为空'})
containers_name = forms.CharField(error_messages={'required': u'此节点不能为空'})
containers_image = forms.CharField(error_messages={'required': u'此节点不能为空'})
表单上输入的东西可能会有很多,根据实际情况哪些字段不能为空就把那些字段写到这个class里,在上面那个YamlInfo
里把这五项配置对应的Django表单字段,比如replicas
,这个字节代表的是容器副本个数
,所以它只能是数字,而且我们不要求它大于100,就设定max为2。
创建完类之后,需要在html页面里根据类的对象创建html标签,然后再提交的时候,需要后台views.py
把前端页面提交的数据封装到一个对象里:obj = YamlInfo(request.POST)
。由于每个Django表单的实例都有一个内置的is_valid()
方法,用来验证接收的数据是否合法。如果所有数据都合法,那么该方法将返回True
,并将所有的表单数据转存到它的一个叫做cleaned_data
的属性中,该属性是以个字典类型数据,然后对这组数据进行展示或者保存到数据库就随你便了;如果有一个数据是非法的,就可以return一个别的结果。
理论到此结束,先看views.py
:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67from django.shortcuts import render,render_to_response
from django.http import HttpResponse
from .models import parameter#引入数据库里的类
from django import forms#引入模块
class YamlInfo(forms.Form): #定义的django表单
name = forms.CharField(error_messages={'required': u'此节点不能为空'},)
replicas = forms.DecimalField(max_digits=2,error_messages={'required': u'副本个数不能大于100'}) #最大只有2位数
labels_app = forms.CharField(error_messages={'required': u'此节点不能为空'})
containers_name = forms.CharField(error_messages={'required': u'此节点不能为空'})
containers_image = forms.CharField(error_messages={'required': u'此节点不能为空'})
#create_yaml就是用来展示输入的页面而已
def create_yaml(request):
obj = YamlInfo()#创建form的对象
return render(request,'create_yaml.html',{'obj':obj})#返回create_yaml这个模板,模板里的内容其实都是空的
#yaml_list就是展示所有的输入情况
def yaml_list(request):
obj = YamlInfo()#创建form的对象
if request.method == 'POST':
input_obj = YamlInfo(request.POST)#request.POST为提交过来的所有数据
if input_obj.is_valid():
data = input_obj.clean()#用clean()函数获取提交的数据
apiVersion = request.POST.get('apiVersion','v1')#POST.get方法获取到非form的对象
kind = request.POST.get('kind','RC')
name = data['name']#用data字典来获取form的对象
replicas = data['replicas']
labels_app = data['labels_app']
containers_name = data['containers_name']
containers_image = data['containers_image']
containerPort1 = request.POST.get('containerPort1',None)
containerPort2 = request.POST.get('containerPort2',None)
containers_name2 = request.POST.get('containers_name2',None)
containers_image2 = request.POST.get('containers_image2',None)
containerPort2_1 = request.POST.get('containerPort2_1',None)
containerPort2_2 = request.POST.get('containerPort2_2',None)
print (data)#可以在后台看到整个data的内容
else:#如果输入不合法,返回错误信息
error_msg = input_obj.errors#errors为错误信息
return render(request,'create_yaml.html',{'obj':input_obj,'errors':error_msg})#将错误信息直接返回到前端页面去展示,刚刚输入的非法字段也保留
else:#如果不是post提交,那么就是展示数据里的情况
yamls = parameter.objects.all().order_by('-id')#以倒数展示,即新加的在上面
context = {}
context['yamls'] = yamls
return render_to_response('yaml_list.html',context)#返回yaml_list.html,里面有数据库的所有数据
Parameter = parameter()#将数据库的类实例化
Parameter.apiVersion = apiVersion
Parameter.kind = kind
Parameter.name = name
Parameter.replicas = replicas
Parameter.labels_app = labels_app
Parameter.containers_name = containers_name
Parameter.containers_image = containers_image
Parameter.containerPort1 = containerPort1
Parameter.containerPort2 = containerPort2
Parameter.containers_name2 = containers_name2
Parameter.containers_image2 = containers_image2
Parameter.containerPort2_1 = containerPort2_1
Parameter.containerPort2_2 = containerPort2_2
Parameter.save() #保存这些到数据库里
yamls = parameter.objects.all().order_by('-id')
context = {}
context['yamls'] = yamls
return render_to_response('yaml_list.html',context)
配置一下urls.py
:
1
2
3
4
5
6
7
8
9from django.contrib import admin
from django.urls import path
from createyaml import views#将app的views.py文件引入
urlpatterns = [
path('admin/', admin.site.urls),#每个页面对应各自在views.py里的函数
path(r'create_yaml/', views.create_yaml),
path(r'yaml_list/', views.yaml_list),
]
配置一下用户输入的界面—create_yaml.html
:
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
32
33
34
35
36
37
38
39<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>生成K8S所用的YAML文件</title>
</head>
<body>
<h1>用户输入</h1>
<h2>请注意!大小写敏感!!!</h2>
<form action="/yaml_list/" method="post" name='yamllist'>
{% csrf_token %}
API版本:
<select name='apiVersion'>
<option value="v1" selected>v1</option>
<option value="extensions/v1beta1">beta1</option>
</select><br />
任务类型:
<select name='kind'>
<option value="Pod" selected>Pod</option>
<option value="Service">Service</option>
<option value="Deployment">Deployment</option>
<option value="ReplicationController">ReplicationController</option>
</select><br />
<p>任务名称:{{ obj.name }} <span>{{ errors.name }}</span></p>
<p>任务数量:{{ obj.replicas }} <span>{{ errors.replicas }}</span></p>
<p>APP名称:{{ obj.labels_app }} <span>{{ errors.labels_app }}</span></p>
<p>容器1名称:{{ obj.containers_name }} <span>{{ errors.containers_name }}</span></p>
<p>容器1镜像:{{ obj.containers_image }} <span>{{ errors.containers_image }}</span></p>
容器1开放端口1:<input type="text" placeholder="没有可以不填" name="containerPort1" /><br />
容器1开放端口2:<input type="text" placeholder="没有可以不填" name="containerPort2" /><br />
容器2名称:<input type="text" placeholder="没有可以不填" name="containers_name2" /><br />
容器2镜像:<input type="text" placeholder="没有可以不填" name="containers_image2" /><br />
容器2开放端口1:<input type="text" placeholder="没有可以不填" name="containerPort2_1" /><br />
容器2开放端口2:<input type="text" placeholder="没有可以不填" name="containerPort2_2" /><br />
<input type="reset" value="清除所有" />
<input type="submit" value="生成yaml文件" />
</form>
</body>
</html>
而跳转后的yaml_list.html
就是这样:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>yaml文件展示</title>
</head>
<body>
<h1>数据库里的yaml数据展示</h1>
<table width="100%" border="1">
<thead>
<a href="http://121.41.37.251:33664/create_yaml/"><button>返回</button></a>
<!--插入按钮 开始-->
<input type="button" value="执行" onclick="MsgBox()" />
<!--插入按钮 结束-->
<!--引用JS代码以达到弹出对话框目的 开始-->
<script language="javascript">
function MsgBox() //声明标识符
{
confirm("确定要执行后台脚本么?"); //弹出对话框
}
</script>
<!--引用JS代码以达到弹出对话框目的 结束-->
<br>
<form>
<tr>
<td align="center">任务序号</td>
<td align="center">yaml名称</td>
<td align="center">api版本</td>
<td align="center">任务类型</td>
<td align="center">任务数量</td>
<td align="center">对应应用</td>
<td align="center">使用的第一个镜像名称</td>
<td align="center">镜像1的第一个端口</td>
<td align="center">镜像1的第二个端口</td>
<td align="center">使用的第二个镜像名称</td>
<td align="center">镜像2的第一个端口</td>
<td align="center">镜像2的第二个端口</td>
</tr>
</thead>
<tbody>
{% for yaml in yamls %}
<tr>
<td><input type="radio" name="id" checked="checked"/>{{ yaml.id }} </td>
<td align="center">{{ yaml.name }} </td>
<td align="center">{{ yaml.apiVersion }}</td>
<td align="center">{{ yaml.kind }}</td>
<td align="center">{{ yaml.replicas }}</td>
<td align="center">{{ yaml.labels_app }}</td>
<td align="center">{{ yaml.containers_image }}</td>
<td align="center">{{ yaml.containerPort1 }}</td>
<td align="center">{{ yaml.containerPort2 }}</td>
<td align="center">{{ yaml.containers_image2 }}</td>
<td align="center">{{ yaml.containerPort2_1 }}</td>
<td align="center">{{ yaml.containerPort2_2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
启动django,我们来看一下效果!
不过说实话,对于用户来说,肯定选择题的感觉要比填空题好。所以到时候我们可以把阿里云容器仓库里的所有的镜像做成一个数据库,到时候映射到这个页面,让用户去在里面做选择而不是填空。而且django的form检查相比较JavaScript而言还是很粗糙的,如果是处女座的话,还是要搞JavaScript,而且两者也并不冲突,一个是对前端用户而言,一个是后台检查录入数据库的。
https://docs.djangoproject.com/en/2.1/topics/forms/ (官方文档)
http://www.liujiangblog.com/course/django/152
https://www.cnblogs.com/chenchao1990/p/5284237.html
http://dokelung-blog.logdown.com/posts/221431-django-notes-8-form-validation-and-modeling
https://www.jb51.net/article/103135.htm
Django里经常会有这样的一个需求—-同样的一组数据要给很多个app使用。比如一个运维系统,运维人员的名单就既要给“项目部署”这个APP用又要给“责任负责人”这个APP用。如果每次都要去跨应用去from XXX.models import xxx
的话,代码感觉很不友好。那么要解决这个问题,就要用到django自带的ContentTypes
框架。以下是所用软件版本:
Django:2.1.1
Python:3.6.4
old app:Articles
new app:read_stats
目前在django的控制台页面的情况是这样的:
可见里面就一个叫Articles
的app,点开之后,发现对应的项目也很简单,只有id
和title
这两个字段而已:
本次试验的目的就是新建立一个文章统计计数的app,在里面配置数据库,然后让原来的blog这个app能够使用得到新app的数据项。
首先先建立一个专门用来计数的app,比如就叫read_stat
。那么就在django项目路径下python manage.py startapp read_stats
,再把这个新的app名称添加到settings.py
里:
1
2
3
4
5
6
7
8
9
10INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'article',#先加载django自身的app,然后是第三方app,最后是自己开发的app
'read_stats',
]
编辑一下read_stats
里的models.py
,创建模型先:
1
2
3
4
5
6
7
8
9
10from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey#这句话是固定的,引用类型
from django.contrib.contenttypes.models import ContentType#这句话是固定的,引用类型
# Create your models here.
class ReadNum(models.Model):
read_num = models.IntegerField(default=0)#设定read_num就是一个普通的数字
content_type = models.ForeignKey(ContentType,on_delete=models.DO_NOTHING)#说明这是一个外键,即关联的模型,加上后面的话的意思是:即使删除了这个字段也不会影响其他数据
object_id = models.PositiveIntegerField()#这里是一个主键,即pk
content_object = GenericForeignKey("content_type","object_id")#通过上面两个变量,配置成一个通用的外键
通过使用一个content_type
属性代替了实际的model
(如Post,Picture),而object_id
则代表了实际model中的一个实例的主键,其中,content_type
和object_id
的字段命名都是作为字符串参数传进content_object
的。
配置了数据库,肯定需要python manage.py makemigrations
和python manage.py migrate
:
数据更新完毕之后,修改一下负责后台展示的admin.py
:
1
2
3
4
5
6
7from django.contrib import admin
from .models import ReadNum#引用ReadNum这个模型
# Register your models here.
@admin.register(ReadNum) #装饰器
class ReadNumAdmin(admin.ModelAdmin):
list_display = ('read_num','content_object')
此时刷新一下django页面就看到read_stats这个app已经注册成功了:
由于是新的,所以里面空空如也,点击一下ADD
,就可以输入值了:Read num
就是设定的“阅读次数”,Content type
这个数据是一个选择项,选择需要对应的数据库模型,即Article这个app里的models.py
的类—Article
,而Object id
就Articles
对应的文章编号:
这样达到了后台配置“将Article应用里的第2篇文章的阅读次数上调到了99次”。
刚才手动在后台配置完毕,但是目前这个read_num
数据只能是在read_stats
这个app里自嗨。要给让Article
能够得到这个read_num
的话,就需要通过模型获取到具体数值,这里要用到ContentType.objects.get_for_model
方法。首先要配置Article
下的models.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from django.db import models
from django.db.models.fields import exceptions#引入错误给try...except使用
from django.contrib.contenttypes.models import ContentType#引入ContentType
from read_stats.models import ReadNum #从另一个app里引入类
# Create your models here.
class Article(models.Model):
title = models.CharField(max_length=30)
content = models.TextField()#这是它原来的数据库内容
#添加一个方法给admin.py使用,如果有就直接返回值(字符串),如果没有object就返回一个0
def get_read_num(self):
try:
ct = ContentType.objects.get_for_model(self)#确定ContentType
readnum = ReadNum.objects.get(content_type=ct,object_id=self.pk)#每个readnum都是content_type和object_id对应的QuerySet
return readnum.read_num#这样返回就是一个具体的值,不然只是一个数据
except exceptions.ObjectDoesNotExist:
return 0
再修改Article
下的admin.py
,让后台可以体现出来read_num
:
1
2
3
4
5
6
7from django.contrib import admin
from .models import Article
# Register your models here.
@admin.register(Article)
class Article(admin.ModelAdmin):
list_display = ('id','title','get_read_num')#这里新加上刚才的那个方法
由于admin.py
里返回的必须是字段,所以我们才在models.py
里添加了一个方法去生成字段。
刷新一下Django后台页面,就看到效果了:
至此,这个read_num
数据就同时被两个APP关联分享了。至于再把read_num
通过一定的处理方法之后映射到html前端就很简单了。
https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/ (官方文档)
]]>python:3.6.5
Django:2.1.1
Project:Kubernetes
,文件夹路径就是/django/Kubernetes/
App:createyaml
,文件夹路径就是/django/Kubernetes/createyaml
前文地址:https://rorschachchan.github.io/2018/09/13/Django%E5%88%B6%E4%BD%9C%E5%89%8D%E7%AB%AF%E9%A1%B5%E9%9D%A2%E7%94%9F%E6%88%90yaml%E6%96%87%E4%BB%B6%E4%B9%8B%E6%94%B9%E8%BF%9B%E7%89%88/
sqlite
是django默认的数据库,如果只是存一点简单的数据,那么它是足够胜任的。如果在django的APP文件夹里配置了models.py
而且执行了python manage.py makemigrations
和python manage.py migrate
的话,那么在project的文件夹里是会生成db.sqlite3
这个文件的。至于如何命令行操作sqlite和python调用sqlite,请去看:http://blog.51cto.com/zengestudy/1904680 ,里面说的已经很清楚了。
不过要注意的是execute
方法得到的是一个对象,是看不到具体的sql结果。还需要fetchall
方法进一步的解析,这样得到的是一个列表,然后取其中的具体元素,如图:
由于yaml的参数是从前端传入的,如果同时有多个人传入数据,那么后端脚本在取参数就会出现错误:多个人在传入不同的数据之后得到的结果却是一样的,即服务器接收到的最后那个数据返回的结果。为了不出现这样的混乱,所以我们就要引入唯一标识符保证每个人得到都是他们的结果。
在数据库里是有一个主键的也就是id
,它是django生成数据库的时候自带的private key
,每一个id都是唯一的,既然唯一那肯定就是我们选做唯一标识符的首选。至于怎么用它,其实就是在原有的views.py
上做一点小手脚。如下:
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
32
33
34
35
36
37
38
39
40
41
42
43
44from django.shortcuts import render
from django.http import HttpResponse
from .models import parameter#引入同级的modes.py里的parameter类
def create_yaml(request):
return render(request,'create_yaml.html')#这个页面是用来输入各值
def get_yaml(request):
if request.method == 'POST':#如果是post传参,那么就记录下来
apiVersion = request.POST.get('apiVersion','v1')
kind = request.POST.get('kind','RC')
name = request.POST.get('name')
replicas = request.POST.get('replicas','1')
labels_app = request.POST.get('labels_app',None)
containers_name = request.POST.get('containers_name',None)
containers_image = request.POST.get('containers_image',None)
containerPort1 = request.POST.get('containerPort1',None)
containerPort2 = request.POST.get('containerPort2',None)
containers_name2 = request.POST.get('containers_name2',None)
containers_image2 = request.POST.get('containers_image2',None)
containerPort2_1 = request.POST.get('containerPort2_1',None)
containerPort2_2 = request.POST.get('containerPort2_2',None)
signer = request.POST.get('signer', 'Micheal Jackson')
else:
return HttpResponse('404')
Parameter = parameter()#将parameter实例化
Parameter.apiVersion = apiVersion#把刚刚从前端得到的值对应赋值
Parameter.kind = kind
Parameter.name = name
Parameter.replicas = replicas
Parameter.labels_app = labels_app
Parameter.containers_name = containers_name
Parameter.containers_image = containers_image
Parameter.containerPort1 = containerPort1
Parameter.containerPort2 = containerPort2
Parameter.containers_name2 = containers_name2
Parameter.containers_image2 = containers_image2
Parameter.containerPort2_1 = containerPort2_1
Parameter.containerPort2_2 = containerPort2_2
Parameter.save() #保存修改
yaml = parameter.objects.get(id=Parameter.id)#通过object.get方法是得到保存的所有值,但是我们只要本次的值,也就是id与private key一致的
return HttpResponse('api版本:%s yaml类型:%s yaml名称:%s 副本数量:%s yaml所属APP:%s 容器名称:%s 容器镜像名:%s' % (yaml.apiVersion,yaml.kind,yaml.name,yaml.replicas,yaml.labels_app,yaml.containers_name,yaml.containers_image)))#输出部分刚输入的值到页面,检查一下是否正确
urls.py
如下:
1
2
3
4
5
6
7
8
9from django.contrib import admin
from django.urls import path
from createyaml import views
urlpatterns = [
path('admin/', admin.site.urls),
path(r'create_yaml/', views.create_yaml),
path(r'get_yaml/', views.get_yaml),
]
启动django,在前端页面测试一下看看是否得到的结果就是本次输入的结果,如图:
可以看到,返回的页面正确的输出了本次各个参数!剩下还有三部分:
http://blog.51cto.com/lannyma/1735751
http://www.liujiangblog.com/course/django/152
https://www.jianshu.com/p/46188b39eae5
以阿里云厂家为例,假设我们有一个网站,它的服务器、数据库、负载均衡都部署在杭州区可用区B,将IP A绑定到某个域名上,启动了系统之后为客户提供服务。那么如果现在要对这套系统进行灾备,应该怎么做?
第一个方法:在可用区D复制一模一样的环境,然后以“主备服务器组”的方式配置一下负载均衡:如果端口监听不正常就会切换到备用服务器上,监听正常了再切回来。但是这个方式有一个问题,就是当前模式阿里云的主备切换是不支持HTTPS/HTTP的,如图:
可见,这种方式是有很大的局限性的。
那既然同是花钱,干脆就做一个异地容灾,整套系统在其他的地理区域比如上海区也复制一遍,把上海区的B IP也绑定到这个网站域名上,阿里云的域名解析是支持多IP绑定同一个域名的。平时的时候,上海区的IP被域名解析的权重是0,一旦杭州区出现了某些线路方面的硬件问题,那么就将杭州区的权重降成0,同时提高上海区的权重,这样用户就会直接访问到上海区的系统。
理想是丰满的,但是现实是骨感的,因为阿里云的权重配置区域是1~100,而不是0~100,如下图:
也就是说这个云解析的负载均衡是不能当做主备切换使用的,如果想要通过阿里云解析来达到主备切换的目的,方法只能是升级VIP DNS,配置网站监控
,具体操作是https://help.aliyun.com/document_detail/59372.html?spm=5176.215331.1147916.23.65de614dac85Sw 。但是这个VIP升级是需要钱的,如果监控的网站越多,花费越大,如果老板不肯掏这份钱,那就只能换条路走。
想来想去,还是老办法—-调用阿里云API修改云解析记录达到切换IP的目的。脚本如下,这里我采取了命令行交互的形式,实际上都是将域名IP写死的:
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
32
33
34
35
36
37
38
#coding=utf-8
#此脚本版本是2.7,用来修改阿里云云解析IP地址,使用之前请先安装sdk:pip install aliyun-python-sdk-domain
import json
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest
print "请注意!本脚本只会修改lechange.com域名下的A记录!!!"
RRKeyWord = raw_input("请输入您要修改的域名:")
Value = raw_input("请输入新的IP:")
client = AcsClient('这里是AK', '这里是SK','cn-hangzhou')
request = CommonRequest()
request.set_accept_format('json')
request.set_domain('alidns.aliyuncs.com')
request.set_method('POST')
request.set_version('2015-01-09')
def getRecordId(RRKeyWord):
global RecordId
request.set_action_name('DescribeDomainRecords')
request.add_query_param('DomainName', 'lechange.com')#这里写死了lechange.com域名
request.add_query_param('RRKeyWord', RRKeyWord)
request.add_query_param('TypeKeyWord', 'A')
response = client.do_action_with_exception(request)
encode_json = json.loads(response)
RecordId = encode_json['DomainRecords']['Record'][0]['RecordId']#需要获取这个RecordId
def UpdateDomainRecord(RRKeyWord,Value):
request.set_action_name('UpdateDomainRecord')
request.add_query_param('RecordId', RecordId)
request.add_query_param('RR', RRKeyWord)
request.add_query_param('Type', 'A')
request.add_query_param('Value', Value)
response = client.do_action_with_exception(request)
if __name__ == "__main__":
getRecordId(RRKeyWord)
UpdateDomainRecord(RRKeyWord,Value)
这个脚本比较粗糙,可以改进的地方如下:
整个脚本启动后效果如下:
https://help.aliyun.com/document_detail/29776.html?spm=a2c4g.11186623.2.37.d31b31dfNqojPT
https://help.aliyun.com/document_detail/44657.html?spm=a2c4g.11186623.6.579.4d1d7cd208aSgl
之前搞了一个简易版的“通过前端页面生成yaml”的方法,地址在此:https://rorschachchan.github.io/2018/09/03/制作前端页面生成yaml文件/ 。但是这个方法实际上有很多的不足,比如说每一次生成记录就消失了,无法追溯,所以要引入数据库,把每一次的数据保存到数据库里。
整体的流程设计还是跟以前的一样:
create_yaml.html
网页让用户输入相关数值,并且有两个按钮,一个是重置,一个是生成yaml供K8s使用;get_yaml
网页,它也有两个按钮,一个是返回,一个是执行此yaml;本篇文章的内容是第一步和第二步,Django的project名是Kubernetes
,app名是createyaml
。
由于这个小系统保存的数据量不多,所以我就直接使用django默认的db.sqlite3
数据库。跑到Kubernetes/createyaml
的models.py
里,根据yaml的实际情况编写一下数据库各字段:
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
32
33from django.db import models
# Create your models here.
class parameter(models.Model):
type = (
(U'Pod','Pod'),
(U'Service','Service'),
(U'Deployment','Deployment'),
(U'ReplicationController','ReplicationController'),
)
api_type = (
(U'v1','v1'),
(U'extensions/v1beta1','beta1'),
)
apiVersion = models.CharField(verbose_name='API版本',max_length=20,choices=api_type)
kind = models.CharField(verbose_name='任务类型',max_length=50,choices=type)
name = models.CharField(verbose_name='任务名称',max_length=100)
replicas = models.CharField(verbose_name='任务数量',max_length=50,default='1')#默认情况下副本数是1
labels_app = models.CharField(verbose_name='APP名称',max_length=100)
containers_name = models.CharField(verbose_name='容器1名称',max_length=100)
containers_image = models.CharField(verbose_name='容器1镜像',max_length=100)
containerPort1 = models.CharField(verbose_name='容器1开放端口1',max_length=25,blank=True)#可以为空,下同
containerPort2 = models.CharField(verbose_name='容器1开放端口2',max_length=25,blank=True)
containers_name2 = models.CharField(verbose_name='容器2名称',max_length=100,blank=True)
containers_image2 = models.CharField(verbose_name='容器2镜像',max_length=100,blank=True)
containerPort2_1 = models.CharField(verbose_name='容器2开放端口1',max_length=25,blank=True)
containerPort2_2 = models.CharField(verbose_name='容器2开放端口2',max_length=25,blank=True)
signer = models.CharField(verbose_name='登记人',max_length=50, default='system')
signtime = models.DateField(auto_now_add= True)#默认添加当前时间
#返回相应的值
def __unicode__(self):
return self.name
保存之后,python manage.py makemigrations
和python manage.py migrate
,就会看到db.sqlite3
文件在Kubernetes
这个project文件夹里诞生了。
根据整体的流程设计所说,url.py就新增了如下几个路由:
1
2
3
4
5urlpatterns = [
path(r'create_yaml/', views.create_yaml),#create_yaml网页里的内容就是views.py里的create_yaml函数,下同
path(r'get_yaml/', views.get_yaml),
path(r'addok/', views.addok),
]
在admin后台界面也要体现出每一次数据输入,于是就配置一下Kubernetes/createyaml/admin.py
:
1
2
3
4
5
6
7
8
9
10
11
12from django.contrib import admin
from .models import parameter#把parameter这个class引入
# Register your models here.
class parameterAdmin(admin.ModelAdmin):
list_display = ('name','apiVersion','kind','replicas','labels_app','containers_name','containers_image','containerPort1','containers_name2','containers_image2','containerPort2_1','signer','signtime')#把models.py里的字段都添加进去
exclude = ['signer']#signer字段不要添加
def save_model(self, request, obj, form, change):
obj.signer = str(request.user)
obj.save()
admin.site.register(parameter,parameterAdmin)
准备工作完事,开始搞前端页面。
在createyaml
文件夹下建立一个template
文件夹,里面先写一个create_yaml.html
:
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
32
33
34
35
36
37
38
39
40<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>生成K8S所用的YAML文件</title>
</head>
<body>
<h1>用户输入:</h1>
<h2>请注意!大小写敏感!!!</h2>
<form action="/get_yaml/" method="post" name='addyaml'>
<!-- form action的意思就是,submit的指向就是/get_yaml/,以post形式传递 -->
{% csrf_token %}
API版本:
<select name='apiVersion'>
<option value="v1" selected>v1</option>
<option value="extensions/v1beta1">beta1</option>
</select><br />
任务类型:
<select name='kind'>
<option value="Pod" selected>Pod</option>
<option value="Service">Service</option>
<option value="Deployment">Deployment</option>
<option value="ReplicationController">ReplicationController</option>
</select><br />
任务名称:<input type="text" name="name" /><br />
任务数量:<input type="text" placeholder="请输入阿拉伯数字" name="replicas" /><br />
APP名称:<input type="text" placeholder="对应的APP" name="labels_app" /><br />
容器1名称:<input type="text" name="containers_name" /><br />
容器1镜像:<input type="text" name="containers_image" /><br />
容器1开放端口1:<input type="text" placeholder="没有可以不填" name="containerPort1" /><br />
容器1开放端口2:<input type="text" placeholder="没有可以不填" name="containerPort2" /><br />
容器2名称:<input type="text" placeholder="没有可以不填" name="containers_name2" /><br />
容器2镜像:<input type="text" placeholder="没有可以不填" name="containers_image2" /><br />
容器2开放端口1:<input type="text" placeholder="没有可以不填" name="containerPort2_1" /><br />
容器2开放端口2:<input type="text" placeholder="没有可以不填" name="containerPort2_2" /><br />
<input type="reset" value="清除所有" />
<input type="submit" value="生成yaml文件" />
</form>
</body>
</html>
写完了之后,再来一个addok.html
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加成功</title>
<style>
* {
margin: 0;
padding: 0;
}
a{
text-decoration:none;
}
</style>
</head>
<body>
<div>
<p>添加成功</p>
</div>
</body>
</html>
前端准备完毕。
views.py里的具体函数是整个django的主心骨,内容如下:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47from django.shortcuts import render
from django.http import HttpResponse,HttpResponseRedirect
def create_yaml(request):
return render(request,'create_yaml.html')#只是展现一个页面而已
def get_yaml(request):
if request.method == 'POST':#如果是POST就获取前端传入的值
apiVersion = request.POST.get('apiVersion','v1')
kind = request.POST.get('kind','RC')
name = request.POST.get('name')
replicas = request.POST.get('replicas','1')
labels_app = request.POST.get('labels_app',None)
containers_name = request.POST.get('containers_name',None)
containers_image = request.POST.get('containers_image',None)
containerPort1 = request.POST.get('containerPort1',None)
containerPort2 = request.POST.get('containerPort2',None)
containers_name2 = request.POST.get('containers_name2',None)
containers_image2 = request.POST.get('containers_image2',None)
containerPort2_1 = request.POST.get('containerPort2_1',None)
containerPort2_2 = request.POST.get('containerPort2_2',None)
signer = request.POST.get('signer', 'Micheal Jackson')
else:
return HttpResponse('404')
from createyaml.models import parameter#数据库对应项进行赋值
Parameter = parameter()
Parameter.apiVersion = apiVersion
Parameter.kind = kind
Parameter.name = name
Parameter.replicas = replicas
Parameter.labels_app = labels_app
Parameter.containers_name = containers_name
Parameter.containers_image = containers_image
Parameter.containerPort1 = containerPort1
Parameter.containerPort2 = containerPort2
Parameter.containers_name2 = containers_name2
Parameter.containers_image2 = containers_image2
Parameter.containerPort2_1 = containerPort2_1
Parameter.containerPort2_2 = containerPort2_2
Parameter.save() #保存到数据库里
# 重定向到添加成功页面
return HttpResponseRedirect('/addok/')
def addok(request):
return render(request,'addok.html')
启动django之后,首先先去admin后台看一下当前的情况,如图:
可以看到里面是有几个记录的,那么我们现在登录外网地址:端口/create_yaml
,输入一些字段看一下效果:
再返回到admin后台刷新,发现刚才新加的任务已经体现出来了:
至此,就达到了“前端html传入数据,后端数据库记录”的效果。
]]>苏振华对本文的初稿提出了中肯的批评和建议,在此致以感谢。
二十世纪中国是一个革命的世纪。二十世纪上半叶,中国经历的主要革命运动有辛亥革命、二次革命、五四运动、北伐战争和共产主义革命。1949年中国共产党取得政权后,又搞了许多具有社会革命性质的社会运动,其中最为著名的有土地改革、人民公社运动、大跃进和文化大革命。改革开放后,中国共产党逐渐从一个革命党转变为执政党,但是中国的一些知识分子、学生和民众却从共产党手中接过“革命的旗帜”,于是就有了1989年的学生运动以及最近的“零八宪章运动”和所谓“茉莉花运动”等集体行动的事件。当然也有知识分子提出中国应该“告别革命”,应该反对激进主义。这是一种应然性吁求,但问题在于:中国是否会再发生(或者能避免)一场革命性的社会动荡?
这一问题甚至引发中国政治精英的广泛关注。最近网上有文章说中共高层有不少人在阅读托克维尔(Alexis de Tocqueville)的《旧制度与大革命》(L’Ancien regime et la Revolution),幷说王岐山看完此书后曾担忧地表示:中国的现代化转型不会那么顺利;中国人自己的代价也没有付够。当然,革命一旦发生,人民将付出的代价在一定程度上是由革命性质决定。一般来说,政治革命(一场只改变政权的性质,而不改变社会经济结构的革命)给社会带来的震荡要远远低于社会革命(一场既改变政权的性质,又改变社会经济结构的革命),非暴力革命给社会带来的震荡要远远低于暴力革命。王岐山也许是在担心中国会发生一场暴力革命,甚至是暴力性的社会革命。
不管上述中共高层读书的传说可信度如何,有一点十分明确:虽然近年来中国政府在维护稳定上花了很大的力气,中国的经济在近三十年来取得了举世瞩目的发展,民众的生活水平在近年来也有了很大的提高,但中共高层丝毫没有减轻对在中国再发生一次革命的可能性的焦虑。中共高层为甚么会如此忧虑?当前中国与政权稳定相关的根本问题是甚么?本文试图在理论的指导下对当前中国面临的困境作出分析。
早期的西方理论都把现代化过程中所发生的巨大社会变迁看作是一个国家发生革命的主要诱导因子。这一理论的逻辑很简单:现代化带来了传统的生活方式和价值观的变化,给身处其中的人们带来很大的不适应和不确定性;同时,现代化过程也削弱了传统社会组织对于人们的控制,给革命造就了机会1。的确,世界上的革命无一不发生在正在发生巨大变化的社会之中,而巨大的社会变迁确实会给身处其境的人们带来多方面的不确定性。从这个意义上说,这种理论自有它的道理。但是,世界上每一个国家在现代化过程中都经历过巨大的社会变迁,却不是每个国家都发生了剧烈的革命。社会变迁充其量只能是引发革命的一个必要条件。
在过去的大多数时间,有些学者也常用阶级或者是利益集团的视角来解释一个国家革命的成功与否2。他们的逻辑也很简单:如果一个国家中的一个主要阶级拥护和加入了革命,那么革命就会成功;反之革命就不会发生,就是发生了也会失败。当今中国的不少学者也仍然会自觉或者不自觉地运用这一视角来分析中国社会的危机所在。依笔者所见,这类分析方法表现出了左派知识分子的天真,而反映出来的则是这些知识分子看待问题时的教条性。
这并不是说人们在现代社会中不会产生阶级认同。问题在于:每一个人在社会上都会同时拥有许多身份(比如一个人同时可具有如下的身份:工人、浙江人、男人、某些圈子中的一员、某个俱乐部的成员等),并且具有某一身份的人们之间又存在着巨大的差别(比如工人之间就有蓝领工和白领工、技术工和非技术工、熟练工和非熟练工、临时工和正式职工之间的差别等)。因此,除非存在巨大无比的外力,比如国家对社会上的一个主要人群的利益完全漠视,幷且对这一人群的抗争进行严厉的和系统性的镇压,否则那些被天真的知识分子所认定的“阶级”就很难形成强烈的认同感,去完成知识分子所赋予他们的“历史使命”。
当今世界只有两类大型群体会有着较为“天然的”强大认同感,那就是族群和宗教群体。他们所发起的抗争和革命也因此往往有较大的威力。在很大的程度上,当今所流行的各种“社会分层研究”都是过去知识分子的研究误区的某种产物。不同的社会分层方法除了对了解社会流动和指导政府的公共政策制订有一定的应用性意义外,从社会行动或革命的角度来看,其价值却十分有限。这当然是题外话。
1970年代后,西方学者开始强调国家的性质和结构性行为对革命产生乃至成功的影响3。这类理论背后的一个核心逻辑是:在当代交通和通讯技术的支持下,现代国家获得了古代国家完全没有的渗透社会的能力。与古代国家相比,现代国家的管治领域不但十分宽泛,而且它的政令更能严重影响到社会上绝大多数成员的利益。现代国家的这一性质导致了如下三个后果:第一,国家的错误政策非常容易触发民众大规模的针对国家的怨恨情绪;第二,国家的强势刺激了人们组织起来进行抗争,要求国家颁布和施行对自己群体有利的法律和社会政策;第三,部分人就会想到通过夺取国家的权力(即革命)来彻底改变国家的性质,通过掌握国家权力来推行他们的理想。在这种所谓“国家中心论”的视角下,西方学者做了大量的研究,幷逐渐产生了以下三点共识(即衡量一个国家发生革命可能性的三个维度):第一,革命不容易发生在一个有着效率较高的官僚集团的国家(官僚集团内的程序政治会增强国家精英的团结、国家决策的合理性和国家镇压机器的有效性);第二,革命不容易发生在一个对社会精英有着很强吸纳能力的国家;第三,革命不容易发生在一个对社会有着很强渗透力(不仅仅指由国家所控制的交通和通讯工具,而且指警察机构对社会的监控能力)的国家4。
以上的三个维度有很强的解释力。的确,早期的革命,包括法国革命(1789)、俄国革命(1917)、中国革命(1949)和伊朗革命(1979),都发生在用以上三个维度来衡量处境都不太妙的国家。其实,官僚集团的效率、国家对社会精英的吸纳能力,以及国家对社会的渗透能力,是任何国家进行有效统治的关键要素。一个没有这些能力或者是这三方面能力不足的现代国家,无论是民主国家还是威权国家,都会在其运行过程中遇到大量的困难。但问题是,长期以来在分析革命的可能性时,西方学者过于借重了这三个因素,因此直到1980年代他们还在强调苏联和东欧国家具有很大的政治稳定性(因为这些国家都有着比较有效率的官僚集团、对社会精英的吸纳能力和对社会的渗透力)5,而完全没有料想到革命竟然马上就在这些国家发生了,而且其中不少国家的革命都取得了成功。
笔者认为,在分析苏联和东欧国家爆发革命的可能性时,西方学者都忽略了国家权力的合法性基础和国家政权稳定性之间的关系这一维度的重要性。具体来说,一个国家的权力愈是建立在较为稳定的合法性基础之上,这一国家就愈不可能发生革命。苏联和东欧之所以发生革命,不仅仅是因为它们的经济没搞好、它们的军事落后、它们在民族问题上走入误区、它们的领导人采取了错误的政策等(这些因素都很重要),而且更在于这些国家没有把政权建立在一个比较稳定的合法性基础之上。笔者多年来对中外各国革命作出分析时不断强调国家的合法性基础与政权稳定性之间的紧密关系6。笔者认为,西方学者所着重的三个维度都是国家统治手段中偏“硬件”性质的成份,而国家的合法性基础和政权稳定性则构成了国家统治的关键性“软件”,它们缺一不可。
国家虽然掌握着强大的官僚组织以及军队与警察等武装力量,但是其统治的有效性仍必须依赖于国家政权在大众(包括国家官员)心目中的合法性。考察古今中外的统治史,我们会发觉国家在寻求统治合法性时只能采取以下三种方式:通过一种价值性的承诺、通过提供公共服务、通过一个普遍被接受的国家领导选拔程序。相应地,我们可以界定三种理想状态的国家合法性基础:意识形态型、绩效型和程序型7。如果一个国家统治的正当性是基于一个被民众广为信仰的价值体系,我们可以说这个国家的统治是基于意识形态合法性;如果一个国家统治的正当性来源于国家向社会提供公共物品的能力时,这个国家的统治则基于绩效合法性;如果一个国家的领导人是通过一个被大多数人所认可的程序而产生,这一国家的统治则基于程序合法性。
需要强调的是,以上定义的是国家合法性来源的三个理想类型(ideal types)。现实中,任何国家都不会把合法性完全建立在某一理想类型之上;或者说,任何国家的合法性来源都是这些理想类型的一个混合体。但是,在某一历史时期内,某一理想类型往往会成为一个国家统治最为重要的基础,幷在很大程度上定义了一个国家的性质。
现在让我们来讨论不同的国家合法性基础和政权稳定性之间的关系。
意识形态是国家统治的一个最为根本的合法性基础。一个国家如果把执政基础完全建立在某一意识形态之上,那是不行的;但是,一个国家的执政如果没有意识形态作为基础,则是万万不行的。当大多数的民众都认同国家所推崇的某一意识形态时,这种意识形态不仅仅为国家的统治提供了道德性依据,而且为社会提供了一个“核心价值观”。如果一个国家有一个被广为接受的核心价值观,统治成本就会大大降低。
需要强调的是,核心价值观不能是“八荣八耻”,也不能是“雷锋精神”,因为这些都只能是一个国家的从属性价值观,只有核心价值观才有助于建立国家的合法性基础。国家的核心价值观必须是一种宏大的给予历史以某种道德意义的叙事(即西方后现代学者所说的“宏大叙事”[grand narrative])。美国中学教科书上所描述的美国建国历史以及那些由建国时期政治家所确定的建国原则和理念,就是核心价值观的一个例子;西周初期所形成的“天命论”以及在西周历史中逐渐得以完善的“宗法制度”是有周一代的核心价值观,幷对古代中国的政治哲学和政治文化产生过重大的影响;当代中国学生在学校里学过的围绕着历史唯物主义和“只有共产党才能救中国”而展开的中国近代史叙事,也是核心价值观的一个例子。当然,美国的宏大叙事在其社会中仍然可以获得广泛的认同,而中国教科书中的叙事方式和内容在国内已经没有多少人真正认同了,幷且中国政府至今也没有创造出一套能被广泛认同的宏大叙事。这一意识形态的缺失所导致的后果就是核心价值观的缺乏,幷给当下中国政府的执政带来了很大的困扰。此是后话。
不同的意识形态有着不同的性质,幷对国家政权的稳定性有着不同的影响。意识形态合法性有三个主要类型:领袖魅力型、世俗意识形态型、宗教意识形态型。在这三个类型中,领袖的魅力(近似于韦伯所说的“克里斯玛合法性”)最不能给予政权一个稳定的合法性基础,因为领袖的寿命有限。
一般来说,世俗意识形态对大众所作的承诺比较容易被验证。一旦当国家不能兑现那些承诺,就会产生合法性危机。从这个意义上来说,世俗意识形态也不是一个稳定的合法性基础。但是如果我们把世俗意识形态进一步细分,就会发觉不同的意识形态对人性有不同的要求和对民众有不同的许诺。一般来说,要是一种意识形态对人性的要求愈接近于人的本性幷且其许诺愈不容易被证伪,这一意识形态就愈能为国家的合法性提供一个可靠的基础。比如美国建立在个人主义基础上的“机会之地”(Land of Opportunity)这一意识形态,不但与人的竞争和趋利本性十分接近,而且很难被证伪。这一意识形态有着人们所说的“钱币落在正面我赢,落在反面你输”(heads I win, tails you lose)的性质:你的成功证明了这意识形态的正确性,而你没有成功很容易被解释为是你没有付出足够或恰当的努力。与之相比较,“共产主义”这一意识形态就很难为一个政权提供稳定的合法性基础。共产主义意识形态不但建立在一个过于理想的人性的基础之上,幷且承诺提供一个比其他社会制度更为完美的世俗世界,例如“各尽所能、按需分配”之类。如果一个国家把共产主义意识形态作为合法性基础,一旦国家不能兑现相应的承诺,民众马上就会产生“信仰危机”,从而给国家带来合法性危机。
但是从理论上来说,即使一个国家把合法性建立在像共产主义这样很不牢靠的意识形态之上,这一国家也是有可能取得较为长久的政权稳定的。这里的诀窍是:当大多数民众还相信这一意识形态时,国家就应该采用选举(程序合法性)来补充共产主义意识形态的内禀不稳定性。因为一旦有了选举,幷且在社会上的大多数民众都认可共产主义意识形态的情况下,当政府搞得不好时,候选人就可以攻击政府没有带领人民在共产主义的“康庄大道”上正确地前进,民众就会去怪罪当朝政府的施政,而不是从意识形态本身的误区来检讨国家中所存在的根本问题。读者可以假设,如果中国在毛泽东时代能搞出一个共产党领导下的民主社会的话,今天的中国也许就不会面临如此严重的意识形态合法性危机。
以上的逻辑还支持了以下的推论:宗教意识形态要比任何世俗意识形态更能为一个国家提供稳定的合法性基础。宗教源自于人的可怜的本性──因为害怕失去和死亡而无限放大生命的意义。宗教的承诺也不具有可验证性──“来世”、“净土”或者“天堂”这样的宗教承诺既十分动人又无法验证,而对于宗教来说,最具权威的克里斯玛都是不存在于世俗世界的“神”、“佛”或者是“圣人”。宗教意识形态与人性的贴近和承诺的无法验证性,赋予那些把国家合法性建基于宗教意识形态之上的国家很大的政权稳定性。
不过,在现代社会,宗教意识形态合法性的最大弱点来自宗教力量和国家政权之间的紧张。现代社会极其复杂且变化极快。为了适应新的变化,国家政权就必须以务实的态度来处理日益复杂的世俗性事物,但是国家的务实态度及其所带来的社会后果势必会招来具有强烈保守倾向的宗教力量的反对。由政教斗争所导致的政权不稳定性,对于那些把宗教意识形态作为合法性基础的国家来说,是必定要面临的一个难题。当今伊朗的政治就在较大程度上受到这一因素的困扰。
任何一个政府都需要为治下的民众提供必要的公共服务,例如仲裁、维持公共秩序、保证人身安全、保卫国家等。这个层面上的绩效是绝不可少的。如果一个政府没有能力提供这些最为基本的公共物品,相应的国家就不会存在,即便存在也会很快垮台。这里所说的“绩效合法性”,指的是国家领导集团在一个更为进取的层面上积极创造绩效以获取合法性。
获取这一合法性的手段可分为三种亚类型:领导经济发展、官员作为民众的道德表率和炒作民族主义情绪。但是,这三种手段都不能为国家提供一个稳定的合法性基础。首先,没有一个国家能保证经济的永久高增长。其次,把官员的道德表率作为国家合法性基础就会将贪污这样在法律层面上能解决的问题提升为政治问题,从而从根本上削弱了国家的合法性。最后,如果在和平时期政府经常以炒作国际危机来提高其统治合法性的话,这一国家的国际环境就会日趋险恶,幷且大量的极端民族主义者就会在这一国家中产生。这将推动一个国家朝着战争的方向发展,后果不堪设想。 总之,当一个国家的合法性系于绩效承诺时,这一国家的政府就必须设法来兑现这些承诺。如果这些绩效承诺得到了兑现,民众的欲望就会提高,幷对政府提出更高的要求,而政府则不得不把民众不断提高的要求作为新的、更新的,甚至是即时的工作目标。但是,一旦政府不能够兑现其承诺时,这一国家马上就会出现合法性危机。
现代社会到来之前,除了古希腊之外,程序始终不是世界各国权力合法性的一个重要基础。这幷不是说在古代政府首脑产生的背后没有程序可言,而是说这些程序只在一小部分精英之间才有意义,幷且这些程序在国家政治中不占有像今天的选举政治般重要的地位。笔者认为,以下三个原因使得程序合法性在现代政治中的地位不断上升:
第一,现代国家绝大多数都采取了政教分离原则,宗教意识形态不再是国家的主要合法性来源,或者说现代国家失去了古代国家所拥有的一个十分稳定的合法性基础;第二,现代国家的政府管理的事情愈来愈多,这就使得绩效在现代国家合法性中的地位大大增强,幷给现代国家的政治带来很大的不稳定性;第三,在现代技术的支持下,政府的统治能力不断加强,民众生活受到国家政策愈来愈严重的影响。在这一背景下,怎么控制政府的权力,幷使之不滥用权力,对广大民众来说就变得十分迫切。
我们可以从多种视角来解释为甚么民主政治会在现代国家中兴起。就本文的角度而言,民主兴起的一个重要原因就是现代国家意识形态合法性不足幷且严重倚重于绩效合法性,这就使得国家不得不依靠程序合法性来获得政权的稳定性。
由于以下原因,现代意义上的程序合法性(即民主选举)会给国家政权带来很大的稳定性8:
第一,一旦国家首脑是由民选产生,只要选举被认为是公正的,执政者即使在上台后表现很差,也不会影响政府执政的合法性。用通俗的话说,在绩效合法性的统治基础上,当官如果不为民作主,就有被赶回家卖红薯的危险;而在程序合法性的统治基础上,当官即使不为民作主,也至少得当完一届才回家卖红薯。从这个意义上说,程序合法性大大减低了民众对政府执政的压力。
第二,当一个国家有了程序合法性后,即使有执政者被赶下台也不是甚么大事。这是因为程序合法性在很大程度上把政府和政体分开了。政府即使垮台(比如水门事件[Watergate Scandal]后的尼克松[Richard M. Nixon]政府),政体也不会受到根本性的动摇。
第三,当一个国家有了程序合法性后,民众的不满在相当程度上可以通过选举或其他常规程序的政府更迭而得到缓解。一旦民众有了选择,他们就难以联合起来进行革命,这也给国家政权带来了稳定性。
第四,一旦当官的不为民作主也没有马上就被赶回家卖红薯的危险的时候,公开批评国家领导就不是甚么大事了,这就给言论和结社自由提供了基础。但这自由同时也约束了人民的行为,缓解了社会矛盾,从而构成了政权稳定的一个重要机制。这是因为言论和结社自由让社会上各种思想及利益的交流和竞争,使人们对社会其他群体的利益有了更深的理解,对社会现状有了现实感。同样重要的是,一旦有了言论和结社自由,现代社会的多样性势必会导致社会组织在利益和观点上的分化,这些组织互相牵制使得任何全民性的革命运动变得不大可能。
但就稳定国家政权而言,程序合法性也有着很多弱点,其中最为重要的是它背后必须有一个核心价值观支撑,或者说只有在竞选各方都服从同一意识形态(即“忠诚反对”)时,程序合法性才能为国家提供政权稳定性。如第二次世界大战前的德国,共产党、纳粹党和社会民主党各自有着完全不同的意识形态,幷且共产党和纳粹党都想利用选举来夺取政权,把国家彻底引向对自己有利的方面,形成赢者通吃的格局,选举在这种情形下就不可能成为国家政权稳定的基础。从这个意义上说,一个政治上最为稳定的国家(或者说最不可能发生革命的国家)应该是一个同时拥有意识形态合法性和程序合法性的国家:程序合法性需要强有力的意识形态合法性的支持,幷且程序合法性又是维持国家的意识形态合法性的关键。
在“世界价值观调查”(World Values Survey)和“亚洲民主动态调查”(Asian Barometer Survey)等调查数据基础上,一些学者对中国的国家合法性进行了研究。他们的一个重要发现是:中国民众对政府的认可度要远远高于许多西方民众对他们政府的认可度。他们于是就得出中国政局稳定、国家具有很高的合法性这一结论9。一般来说,我们都会相信这些研究的结论是成立的。这些学者都受过严格的西方学术训练,他们的材料所展示的也是全国民众的普遍看法,而不是少数人的极端观点。同时,中国政府近年来加强了吏治,采取了一系列的“亲民政策”,这些政策应该说是取得一定效果的。笔者近年来在全国范围内与农村和城市的各界民众进行了不少交流,感到中国百姓的生活水平在近年来有了普遍的和显著的提高,或者说大多数百姓确实从国家的政策中获得了实惠。这些学者的研究结果所反映的正是民众对于当今政府的绩效在一定程度上的认可。
但问题是,从“百姓对当下政府的绩效是肯定的”这一现象中,我们是不能推论出“这个国家的政局是稳定的”这样一个结论的。遍览世界各国,民众对政府绩效的评价,可以说是说变就变的。在西方,民众对政府的认可度数月内就可以波动许多个百分点(他们对政府的认可度有时甚至低至百分之十几)。在西方国家,民众对政府绩效的认可度与国家政局的稳定性之间没有很大的关系,因为西方国家合法性的根本基础不是政府的绩效,而是被主流精英和人民所认可的核心价值观和具有程序公正的选举。但是在中国,百姓对政府执政绩效的认可度与政局的稳定却有着密切的关系:如果中国百姓对政府绩效的认可显著下跌的话,的确是有可能引发一场大规模的政治波动甚至革命的。这背后的原因很简单:共产主义意识形态在中国已经式微,但是国家又拿不出其他有效的价值观取而代之;同时,中国领导人也不是通过一种被大多数人所认可的程序而产生的。中国因此非常缺乏意识形态和程序层面上的合法性,于是绩效就成了国家合法性的最为重要、甚至是唯一的基础。
中国经济发展举世瞩目,百姓的生活水平近年来有了很大的提高。但是,中国维稳的成本却愈来愈高。2011年,中国一些人受到突尼斯“茉莉花革命”的影响,促动“茉莉花运动”,但国内几乎没有人响应。尽管如此,不少市政府还是如临大敌,弄得马路上的警察人数不知超过了寥寥无几的闹事人群多少倍。显然,繁荣的经济和大多数百姓对当下政府在不少方面的表现还算满意这些事实,完全不能减轻中共高层领导的焦虑。到底甚么是当前中国政局的关键性不稳定因素?或者问:中共高层领导到底在忧虑甚么?说到这一点,国内的绝大多数知识分子和百姓都会把诸如贫富差距过大、官员贪污腐败等放在首列,但这些因素的重要性或许幷不是想象般大。当前中国的贫富差距的确很大,而官员贪污腐败(特别是在那些吏治较差的省份)无疑也十分严重。相比之下,印度的贫富差距和官员腐败也十分厉害,甚至在不少方面明显超过了中国,可是印度却不是人们认为很可能发生革命的国家。显然,仅仅是贫富差距和官员贪污腐败是不足以引发革命的。
中国的知识分子和百姓都对贫富差距和官员腐败深恶痛绝,但是中国却完全不存在这方面的高质量研究。于是,在考虑这些问题时,中国的知识分子和民众就不得不凭借想象:你对政府有多大程度上的不信任,你就会把中国的贫富差距和官员腐败问题想象得有多严重。笔者认为,当前中国的问题归根到底是政治问题,或者说国家的合法性问题,而不是诸如贫富差距和官员腐败这类社会问题。而中共政权合法性问题的关键在于:第一,国家在共产主义意识形态式微后再也拿不出一个能被广泛认可的主流价值体系;第二,国家不敢(或者不愿意)把合法性的重心转移到程序合法性的层面上来;第三,国家对于绩效合法性产生了过度的依赖。
当下中国的领导人似乎仍然不了解绩效合法性的内禀不稳定这一特质,因为在他们的各种发言中不断流露出人民自然会拥护一个绩效优良的政府这样一种天真的论点,幷且他们也正在努力地通过加强政府绩效来获取国家的合法性。他们的做法与百姓情绪的耦合就给中国带来了如下的悖论:中国的经济和民众的生活水平在近年来都取得了举世羡慕的发展,但是社会却有朝着革命方向发展的倾向。
当社会上的大多数精英和百姓都认同于国家建构的意识形态时,这一意识形态就会成为一个社会的核心价值观或者说核心意识形态。在有着主流意识形态的国家中,社会就会显得非常平和甚至是保守。比如媒体:如果一个记者经常在某一媒体上发表与主流意识形态不符的言论,百姓就会不喜欢这个媒体,其订阅量或收视率就会下降,媒体老板也因此会不喜欢这一记者。可以说,当国家建构的主流意识形态被广为接受时,百姓就会更相信那些平和甚至是保守的报导,而发表偏激言论的媒体就会没有出路。个体也一样:如果一个人经常在公开场合(和网络上)发表与主流意识形态不符的言论,他的言论就会被忽视,他的朋友也不会喜欢他,他也不会有任何社会影响。但是,如果社会上的精英和大多数百姓不认同国家建构的主流意识形态时,人们就会不相信主流媒体中的报导,特别是与政治有关的报导,与主流意识形态保持一致的媒体就会在民众的心目中被边缘化,幷且不再能建构民众的舆论,而敢于反对主流意识形态的媒体和个人就会被看作是“社会的良知”。
当国家建构的意识形态不再是社会上的主流价值观时,在面对以上的异议时国家也就失去有效的对策。如果国家对闹事者或者发表对国家不满观点的人士进行镇压的话,那么国家政权在民众心目中就会进一步失去道义,稍有良知的国家干部就会感觉愧疚,而闹事者和发表对国家不满观点的人士就会被大家看作是“英雄”。但是如果国家选择容忍的话,那么这些人的行动和言论就得不到约束。更有之,一旦形成了这样的“机会结构”,人们就会发觉“会闹的孩子多吃奶”这一妙诀,社会民风于是趋于民粹和暴戾。同时,一旦大众有着把闹事者和发表对国家强烈不满观点的人士看作是“英雄”的倾向,随着“英雄”形象而产生的种种利益就会刺激有些人带着寻租的心态去装扮“英雄”。社会道德就在围绕着反体制而产生的种种“高尚”话语下不断下降。
当国家建构的意识形态不再是社会上的主流价值观时,政府就会失去公信力。这时,如果国家对舆论不加控制,反政府的言论就会在社会上产生很大的影响力,从而引发政治危机。但是如果国家控制舆论的话,人们就会去追逐谣言;加上长期控制舆论而导致人们普遍的无知,天方夜谭式的谣言很容易不胫而走,比如“江泽民去世了,但是中共却秘不发葬”、“薄熙来手上有一百多条人命”、“被重庆警察击毙的不是周克华而是一个便衣警察”等,也会被大家(包括不少社会精英)津津乐道。这些传言不但会给中国的政局增加不确定因素,幷且使得中国本来就很糟糕的政治文化进一步走向糜烂。
当国家建构的意识形态不再是社会上的主流意识形态时,国家的当权者甚至不敢运用民主选举来增强其合法性。从当权者的私利角度看,在这样的情况下举行选举不但会使他们马上下台,而且整个共产党的统治也会结束;很少有当权者愿意在这样的条件下推动民主选举。而从国家利益来说,如果政治精英不能服从一个主流价值观,由选举而产生的“非忠诚反对派”就会撕裂社会,这给了当局拒绝搞民主选举以一定的道德依据。但接下来的问题是,不搞以选举为核心的程序政治只会使得社会矛盾不断积累,幷为中国从威权国家到民主国家的平稳过渡增加了难度。
一旦国家的合法性不能依托于意识形态和领导人的产生程序,绩效就成了国家唯一可依托的合法性基础。得益于中国的“强国家”传统,中国政府在加强执政绩效方面应该说还是可圈可点的。但是,即便可圈可点的绩效使得中国政府变得十分富有,其后果却是金钱使国家领导变得短视,以为金钱能解决一切问题,结果在解决一个问题的同时制造了几个问题。更令人担忧的是,围绕着金钱所产生的种种利益,使得大量的利益相关者带着工具理性围聚在政府周围。这些人对体制毫无忠诚可言,他们一方面死死地把住体制的大船,另一方面则随时准备另寻高就甚至搞狡兔三窟。当前中国出现了“裸官”现象,即不少国家干部的妻子和子女都在国外拥有永久居住权甚至是公民资格,大多数年轻人都向往公务员和国企的工作,其原因盖出于此。这带来的后果就是当前中国民众的强烈仇官心理以及由此生发出来的对任何成功者的仇恨心理,整个社会的道德维系(moral fabric)被大面积毁坏。
为了进一步加强绩效合法性,政府就必须加强吏治、采取悦民政策,幷且把社会上可能出现各种不安定因素的事情统统管了起来。但是,恶性循环不可避免地开始了:政府管得愈好,民众对政府的要求就会愈高;政府管得愈多,问题也就愈多,很多社会问题于是成了政治问题。社会问题的重新政治化是近十年来中国出现的一个令人担忧的发展方向。
在国内,对国家前途不看好的还真是大有人在,其中既有国内语境下的“自由主义者”和比较极端的“左派”,也有难以计数的掌握着一定话语权的网民。最近,甚至连吴敬琏这样比较持重的学者,都在发表文章惊呼当前中国的“经济社会矛盾几乎到了临界点”10。本文认为,中国的确有再爆发一次革命的可能。与以上的观点不同是,笔者认为当这场动荡到来时,其引发的根本原因不应该是当今中国社会上存在着的各种“经济社会矛盾”,而是民众在主观层面上的不满情绪以及由此带来的大量的社会矛盾。而这些不满情绪和社会矛盾的根源,则是当今政府在国家的法律─选举合法性不足的情况下,过多地把绩效当作了国家合法性的根本基础。笔者同时认为,虽然当前的形势很严峻,但是由于以下原因,中国并没有马上就爆发一场革命的危险:
第一,尽管近年来中国经济发展的势头有所减缓,但是中国仍然是世界上经济发展最为迅速、百姓生活水平有着快速提高的国家。只要中国经济继续能保持目前的增长势头,绩效合法性就还能维持一定的效力,一场革命性的动荡在中国就暂时不会发生。
第二,在中国的不少地区(特别是藏区和新疆地区)有着很严重的民族问题,但中国少数民族人口与汉人相比比例实在太小;这就是说,与前苏联不同,少数民族地区的动乱在中国不会是引发革命的一个主要动因。
第三,由于美国经济的衰退和美国对外政策在世界上普遍不得人心,相当部分的中国知识分子不再简单地把美国政治和政治体制作为理想,或者说当前中国的“自由派”知识分子不再享有1980年代的道德高度,因此也失去了1980年代一呼百应的能力。
第四,中国知识分子在近年来生活水平有了很大的提高,幷且他们发表言论的渠道也大大增加。如果说前一个变化给了知识分子耐心,使他们不会急于鼓动革命,后一个变化则促进了知识群体的分化,从而降低了在中国产生一个人们广为接受的反体制意识形态的可能性。第五,国内外大多数的学者往往会把中国每天都在发生的群体性抗争事件(特别是一些重大事件)看作为革命性事件的可能促发因素。这种观点再一次反映了知识分子的天真。笔者认为,大量的群体性事件对中国政治的稳定实际上有着巨大的正面作用。当前不少地方的地方政府软弱,中国大规模爆发群体性抗争事件的阈值因此较低,社会矛盾也不容易有大规模的堆积。此外,当前中央政府对地方发生的群体性抗争事件采取的基本态度就是让地方政府自己去处理。只要地方政府能控制住局面,中央就保持袖手旁观的姿态;但是如果地方政府让事件失控,或者在处理过程中造成了流血事件,在国内外引起广泛关注,中央政府则会对地方政府官员进行处罚。中央政府的这一做法强化了群体性事件参加者“反贪官不反皇帝”的心态,同时也促使地方政府在处理群体性事件时表现出了极大的多样性和灵活性,从而大大缓解了中国群体性事件走向政治化的倾向。
第六,与一些领袖终身制的国家相比,中国已经形成了一套比较成型的国家领导每届五年,每任不超过两届的做法。虽然新的领导人不是由普选产生,幷且换届过程的不透明也给各种政治流言提供了温床,但是换届送走了人们已经厌烦了的领导(不在于干得好不好,而在于一个人在领导位置坐长了人们都会产生厌倦感),给了人们一种新的想象和希望,从而缓解了社会矛盾朝着革命的方向发展。
但是以上这些有利于缓解社会矛盾激化的因素,完全不可能改变以下的事实:在意识形态和程序合法性严重不足的情况下,执政绩效成了当前中国政府最为主要的合法性基础。因此,即便中国没有马上就发生革命性动荡的危险,只要国家的性质得不到根本性的改变,再发生一次革命的危险在中国始终存在。从这个意义上来说,“中国人自己的代价”的确“没有付够”。
1 Samuel P. Huntington, Political Order in Changing Societies (New Haven, CT: Yale University Press, 1968); William Kornhauser, The Politics of Mass Society (Glencoe, IL: Free Press, 1959); Eric R. Wolf, “Peasant Rebellion and Revolution”, in National Liberation: Revolution in the Third World, ed. Norman Miller and Roderick Aya (New York: Free Press, 1971), 48-67.
2 Barrington Moore, Social Origins of Dictatorship and Democracy: Lord and Peasant in the Making of the Modern World (Boston: Beacon Press, 1966); Jeffrey M. Paige, Agrarian Revolution: Social Movements and Export Agriculture in the Underdeveloped World (New York: Free Press, 1975).
3 Jeff Goodwin, No Other Way Out: States and Revolutionary Movements, 1945-1991 (New York: Cambridge University Press, 2001); Tim McDaniel, Autocracy, Capitalism, and Revolution in Russia (Berkeley, CA: University of California Press, 1988); Autocracy, Modernization, and Revolution in Russia and Iran (Princeton, NJ: Princeton University Press, 1991); Theda Skocpol, States and Social Revolutions: A Comparative Analysis of France, Russia, and China (New York: Cambridge University Press, 1979); Timothy P. Wickham-Crowley, Guerrillas and Revolution in Latin America: A Comparative Study of Insurgents and Regimes since 1956 (Princeton, NJ: Princeton University Press, 1992).
4、5 Jeff Goodwin and Theda Skocpol, “Explaining Revolutions in the Contemporary Third World”, Politics and Society 17, no. 4 (1989): 489-509.
6 Dingxin Zhao, “State Legitimacy, State Policy, and the Development of the 1989 Beijing Student Movement”, Asian Perspective 23, no. 2 (1999): 245-84; “State-Society Relations and the Discourses and Activities of the 1989 Beijing Student Movement”, American Journal of Sociology 105, no. 6 (2000): 1592-632.
7 Dingxin Zhao, The Power of Tiananmen: State-Society Relations and the 1989 Beijing Student Movement (Chicago: University of Chicago Press, 2001); “The Mandate of Heaven and Performance Legitimation in Historical and Contemporary China”, American Behavioral Scientist 53, no. 3 (2009): 416-33.
8 赵鼎新:〈民主的生命力、局限与中国的出路〉,《领导者》,2007年第18期,页76-86。
9 Jie Chen, Popular Political Support in Urban China (Washington, DC: Woodrow Wilson Center Press; Stanford, CA: Stanford University Press, 2004); Bruce Gilley, “Legitimacy and Institutional Change: The Case of China”, Comparative Political Studies 41, no. 3 (2008): 259-84; Lianjiang Li, “Political Trust in Rural China”, Modern China 30, no. 2 (2004): 228-58; Tianjian Shi, “Cultural Values and Political Trust: A Comparison of the People’s Republic of China and Taiwan”, Comparative Politics 33, no. 4 (2001): 401-19; Wenfang Tang, Public Opinion and Political Change in China (Stanford, CA: Stanford University Press, 2005).
10 参见吴敬琏的博客,http://wujinglianblog.i.sohu.com/blog/view/236115860.htm。
]]>整体流程如下图,请感受灵魂画师的功力:
Django:2.1.1
,阿里云服务器
Python:3.6.4
,安装方法见:https://rorschachchan.github.io/2018/07/31/获取网站title的脚本/
由于是python3,所以直接pip install django
就安装最新的Django版本。
1
2
3
4
5django-admin startproject Kubernetes#如果提示django-admin命令不存在可以做一个软连接到/usr/local/bin/目录下
cd Kubernetes
python manage.py startapp createyaml #创建APP
python manage.py migrate
python manage.py createsuperuser
app创建完毕之后,在Kubernetes/settings.py
的INSTALLED_APPS
字段添加createyaml
,此时就创建好了项目和app。python manage.py runserver 0.0.0.0:8000
启动django,然后浏览器地址栏输入外网IP:8000
,就会看到django正常启动了,如图:
首先我们先准备一个脚本111.sh
,这个脚本很简单,就是接收到前端传入的数值然后加工成一个yaml文件,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#用来生成对应的yaml文件
cat << EOF
=============================
=== HERE IS YOUR YAML ===
=============================
EOF
echo apiVersion: v1
echo kind: $1
echo metadata:
echo name: $2
echo labels:
echo app: web
echo spec:
echo containers:
echo -- name: front-end
echo image: $5
echo ports:
echo -- containerPort: $3
echo -- name: rss-reader
echo image: nickchase/rss-php-nginx:v1
echo ports:
echo - containerPort: $4
可以看出上面这个生成yaml脚本太粗糙了,很多地方还有待改进,但是这仅仅是一个小例子而已。再去/django/Kubernetes/createyaml/templates
里准备一个比较简单的前端页面脚本,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>创建yaml文件</title>
</head>
<body>
<h1>创建YAML文件用于K8s部署</h1>
<h2>请根据实际情况填写以下内容</h2>
<form method="post" action="/create_yaml/">
<input type="text" name="kind" placeholder="类型"><br>
<input type="text" name="name" placeholder="名称"><br>
<input type="text" name="containerPort1" placeholder="容器端口1"><br>
<input type="text" name="containerPort2" placeholder="容器端口2"><br>
<input type="text" name="mirror" placeholder="镜像"><br>
{{ error }}<br>
<button id="btn" type="submit">生成yaml</button>
{% csrf_token %}
<!-- 标签添加CSRF令牌,这是因为django针对CSRF(跨站请求伪造)有保护措施,没有这句话就是403 --!>
</form>
</body>
</html>
有了页面,还需要一个域名指向这个页面,修改一下/django/Kubernetes/Kubernetes/urls.py
,改成如下:
1
2
3
4
5
6
7
8from django.contrib import admin
from django.urls import path
from createyaml import views#将createyaml这个app的views引进
urlpatterns = [
path('admin/', admin.site.urls),
path(r'create_yaml/', views.create_yaml),#新版的这里不再是url了,把这个url指向views.py里的create_yaml函数
]
再继续,写一下views.py
里的create_yaml
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import subprocess#引入这个库
#创建yaml
def create_yaml(request):
if request.method == 'POST':
kind = request.POST.get('kind', '')#后面的''是默认值的意思
name = request.POST.get('name', '')
containerPort1 = request.POST.get('containerPort1', '')
containerPort2 = request.POST.get('containerPort2', '')
mirror = request.POST.get('mirror', '')
result = subprocess.Popen(args=['bash','/docker/111.sh',name,mirror,containerPort1,containerPort2],stdout = subprocess.PIPE,shell = False).stdout.read()#在这里通过subprocess去启动111.sh这个脚本
return HttpResponse(result,content_type="text/plain")
else:
return render(request,'createyaml.html')
以上函数多说几句:
POST
,不是的话返回该页面;request.POST.get
方法获取前端传入的名称或者端口等值,此处的kind
、name
、mirror
和containerPort
就是html文件里form表单部分那两个input
标签的name
属性;subprocess
来调用111.sh来用这些变量去运行脚本,执行的结果就是result
,然后return
这个result
结果;shell = True
,因为这样的话,要是不小心rm -rf /
,你就gg了,但是如果shell = False
的话,就会把刚才的命令看成rm
和-rf /
两部分,也就是不能成功,这样也免去了别人恶意注入的危险;https://blog.csdn.net/xiaoyaozizai017/article/details/72794469
http://lipeilipei.top/2018/02/07/python+django%E5%AE%9E%E7%8E%B0%E7%99%BB%E9%99%86%E5%8A%9F%E8%83%BD%EF%BC%88%E4%B8%8B%E7%AF%87%EF%BC%89/
https://blog.csdn.net/bjbz_cxy/article/details/79358718 (如果不想用django就可以看看这个cgi方法)
http://blog.51cto.com/laomomo/2163399
把k8s引入到整个部署的自动化流程如下图:
上图已经说的很明白了,但是结合到我公司的内部情况,再加一点文字的解释:
Jenkins:2.124
,jenkins与docker在同一台云服务器上,并且确定这个机器上可以顺利login到阿里云的私有仓库
云存储:阿里云OSS
Gitlab:10.7.3
镜像仓库:阿里云容器镜像仓库
钉钉:4.5.5
既然要让jenkins调用钉钉发送成功消息,那么就需要把jenkins跟钉钉结合在一起。至于怎么配自定义钉钉机器人,请看钉钉的官方文档:https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.karFPe&treeId=257&articleId=105735&docType=1 。而jenkins里也是有官方的钉钉插件,界面系统管理
–管理插件
,然后搜索“dingding”,安装即可,如图:
插件安装完毕之后,重启jenkins即可。
阿里云官方挂载云存储的方法是ossfs
,登陆到jenkins所在的服务器(centos 7.4)里,步骤如下:
1
2
3
4
5
6wget https://github.com/aliyun/ossfs/releases/download/v1.80.5/ossfs_1.80.5_centos7.0_x86_64.rpm
yum localinstall ossfs_1.80.5_centos7.0_x86_64.rpm #这一步安装可能会比较慢
echo 需要挂载的bucket名:云存储对应ak:云存储对应sk > /etc/passwd-ossfs#将云存储的ak,sk写入到文件里
chmod 640 /etc/passwd-ossfs
mkdir /tmp/ossfs#创建挂载文件
ossfs 需要挂载的bucket名 /tmp/ossfs -ourl=http://oss-cn-hangzhou-internal.aliyuncs.com#如果不是阿里云就要用外网的endpoint
操作的效果如下,我挂载的bucket叫ligentest
,毕竟代码是高度机密,bucket属性设置是私有
,256T的容量爽爽的:
在jenkins里创建一个新的工程,取名叫“构建镜像并且上传到云仓库”。“gitlab更新就触发jenkins”的配置内容可以参考 https://rorschachchan.github.io/2018/05/25/Gitlab-Jenkins搭建持续集成系统/ 一文进行操作。
配置正确jenkins与gitlab各自的webhook,测试提交能返回200之后。就要配置构建
和构建后操作
。
构建
选择执行shell
,里面填写这样一个命令:sudo sh /docker/pushimage.sh
,也就是运行一个脚本,脚本内容如下:
1
2
3
4
5
6
7
8
#这个脚本用来推送最新的镜像去阿里云镜像仓库
version=$(date +20%y%m%d)#用当前日期作为version
docker build -f /docker/chenpyfile -t chentest/python:$version .#先本地构建镜像
image_id=$(docker images | awk '{print $3}' | sed -n '2p')#获取image的id号
docker tag $image_id registry.cn-hangzhou.aliyuncs.com/lechangetest/chentest:$version#给本地的镜像打一个tag
docker push registry.cn-hangzhou.aliyuncs.com/lechangetest/chentest:$version#推送到阿里云对应的仓库去
构建后操作
选择钉钉通知器配置
,jenkins URL
一栏应该默认填好的,即jenkins的网址;钉钉access token
这一栏就是直接填机器人的那个access token
,然后选择根据什么情景机器人触发通知,如图:
首先要确认jenkins用户能否正常使用docker命令,方法就是修改一下/etc/sudoers
添加jenkins
这个用户即可。
这次测试,我们就不搞nginx那种静态页面了,换一个python在后台运行的例子。首先,准备一个叫time.py
的脚本,这个脚本很简单,就是不断的输出当前时间的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
#coding=utf-8
import time
def get_time():
localtime = time.asctime( time.localtime(time.time()) )
print ("本地时间为 :", localtime)#python的dockerfile用的是latest,python3是要求有括号的
if __name__ == '__main__':
while True:
get_time()
time.sleep(1)
对应的dockerfile叫chenpyfile
,如下:
1
2
3
4
5
6
7
8
9
10
11
12############################################################
# Dockerfile to build A python container images #
# Based on Python #
############################################################
FROM python:latest
MAINTAINER ChrisChan "Chris@jjfjj.com"
RUN apt-get update && \
apt-get install -y vim && \
apt-get install -y procps
RUN mkdir -p /root/app
COPY /script/ /root/script#把上面那个脚本拷贝到容器里,当然挂载也可以
CMD ["python", "/root/script/time.py"]#这里不要写“python /root/script/time.py”,注意前后台问题
这个dockerfile在本地测试构建镜像是完全没问题的,然后触发一下git push
,就会看到钉钉机器人启动了:
构建完毕之后,机器人也会给一个成功的标志,然后去阿里云的云仓库一看,嗯,果然已经推送过来了!如图:
再docker run -dit --name chen-pytest registry.cn-hangzhou.aliyuncs.com/lechangetest/chentest:20180831
,也能看到新创建的镜像是可以启动的:
至此整个“Jenkins自动构建镜像并且发送钉钉通知”部分就结束了。
https://jimmysong.io/posts/kubernetes-jenkins-ci-cd/
https://help.aliyun.com/document_detail/32196.html
http://www.cnblogs.com/jianxuanbing/p/7211006.html
kubenetes:阿里云服务,版本v1.10.4
,三个master,一个node,我也不知道为啥阿里云设定master最少是3个,而node最少可以是1个…
服务器:阿里云Centos 7.4
首先,我们先部署一个以dockhub最新nginx镜像为底的nginx。命令如下:kubectl run nginx-test --image=nginx:latest --port=80
。同理,再部署一个最新版redis的话,就是找葫芦画瓢:kubectl run redis-test --image=redis:latest --port=6379
。
两个命令敲完,这就给k8s下达了一个deployment(部署任务),可用kubectl get deployments
和kubectl get pods
命令查看:
可以看到现在已经生成了对应的pod,而pod里就是容器了,容器里就是对应的服务。如果想爬进这个容器看一下里面的文件等情况,命令是:kubectl exec -it nginx-test-bb95c4645-7qpbj bash
。
这里插播一下kubectl get deployment
里各参数的含义:
1
2
3
4DESIRED:对应.spec.replicas,用户设定的期望副本数
CURRENT:对应.status.replicas,目前运行的副本数
UP-TO-DATE:对应.status.updatedReplicas,包含最新的pod template的副本数
AVAILABLE:对应.status.availableReplicas,进入正常状态的副本数
但是现在这个服务是外网无法访问的,因为宿主机还没有一个端口与这个nginx容器的80端口相对应。所以要暴露一个端口给外部用于访问。命令是:kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 80
,然后用kubectl get services
查看一下效果:
然后在对应的master和node里就看到宿主机随机分配的那个30497端口已经启动了,如图:
在浏览器上访问一下30497端口,果然可以访问到nginx服务:
服务嘛,总有高峰低谷。比如微博,突然爆出来哪个娱乐明星的新闻,肯定就会有大量的流量涌入,此时就需要扩容,那么k8s的扩容很简单,就是pod的复制,如果要把上面那个nginx-test的部署任务进行扩展,命令就是kubectl scale deployments/nginx-test --replicas=4
,如图:
可见nginx-test又生成了三个pod,与原来的组成了4个pod,而另一个redis的部署任务是没有变化的。
用kubectl get pods -o wide
可见,每一个pod分配到了不同的虚拟IP上,而且node都是阿里云的那台node服务器。
在阿里云控制台也能看到里面的情况:
此时进入到node节点,docker ps -a
就会看到新的nginx景象生成,同时也生成了三个/pause
的容器:
kubernetes中的pause容器主要为每个业务容器提供以下功能:
注意!目前kubernetes似乎仅仅支持共享网络,还不支持进程体系、文件系统之间的共享。如果此时在访问,就会看到访问会相对均匀的落到这四个pod中的每一个,起到一个负载均衡的作用。如果高峰期过了,不需要那么多pod了,就kubectl scale deployments/nginx-test --replicas=1
,pod就会恢复成1个,据我几次试验,每次都是保留最老的那一个pod。
K8s的yaml文件的文法和规矩,官方社区就有教程:https://www.kubernetes.org.cn/1414.html 。但是如果要搭配阿里云的私有镜像,需要先参考一下阿里云文档:https://help.aliyun.com/document_detail/86562.html 。注意,这个方法不能在命令行里使用,只能在yaml或者json里用。这里先写一个简单的nginx配置文件pod-nginx.yaml
做例子,全文如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20---
apiVersion: v1
kind: Pod
metadata:
name: aliyun-nginx
labels:
app: web
spec:
restartPolicy: Always #表明该容器一直运行,默认k8s的策略,在此容器退出后,会立即创建一个相同的容器
nodeSelector:
zone: node1#节点选择
containers:
- name: aliyun-test-nginx
image: registry-vpc.cn-hangzhou.aliyuncs.com/lechangetest/chentest:1.1
imagePullPolicy: IfNotPresent #可选择Always、Never、IfNotPresent,即每次启动时检查和更新images的策略,IfNotPresent是节点上没有此nginx镜像时才执行pull操作
ports:
- containerPort: 80 #容器开发对外的端口
hostPort: 33664 #映射到主机的端口/对外映射的端口(一般可以不写)
imagePullSecrets:
- name: regsecret#这句话为了通过阿里云似有仓库的鉴权
保存退出,再kubectl create -f pod-redis.yaml
把这个文件执行一下。然后kubectl get pod
看一下效果:
发现我们创建那个redis-pod状态是Pending
(等待中),那就是不成功啊。于是就kubectl describe pod/pod-redis
查看一下原因,反馈如下:
这个错误的意思是“如果指定的label在所有node上都无法匹配,则创建Pod失败”。原来是我没有配置kubectl label nodes
,那先把pod-redis
删除,再把nodeSelector
那一段去掉,改成nodeName: cn-hangzhou.i-bp1978gmunq3oalfcqlx
,去掉再重新create一下。kubectl get pod
检查:
然后就是给这个pod增加一个对外的端口。kubectl expose pod/aliyun-nginx --type="NodePort" --port 80
,效果如下:
再去浏览器里,输入node的外网网址:31829
看看效果:
配置成功,当然这整个过程也可以在阿里云的控制台操作,更简单更直观,而且阿里云还会自动把对外端口配置到SLB里,具体步骤可以看阿里云的官方文档。
假设我们把nginx-test
这个deployment的镜像升级成阿里云私有仓库的1.1版本,那么命令是:
1
kubectl set image deployments/nginx-test nginx-test=registry.cn-hangzhou.aliyuncs.com/lechangetest/chentest:1.1
升级之后,kubectl get pod
发现有几个节点不正常,如图:
那么这种情况下需要紧急回滚,回滚命令:
1
kubectl rollout undo deployment/nginx-test
一会就看到回滚成功了。如图:
https://jimmysong.io/posts/what-is-a-pause-container/
https://blog.csdn.net/mailjoin/article/details/79686937
http://pipul.org/2016/05/why-we-need-the-pod-and-service-of-kubernetes/
https://www.imooc.com/article/30473