em0t

em0t 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

Talk is cheap, show me the code

个人动态

em0t 赞了文章 · 2020-08-03

我花了一个五一终于搞懂了OpenLDAP

轻型目录访问协议(英文:Lightweight Directory Access Protocol,缩写:LDAP)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

OpenLDAP是轻型目录访问协议(Lightweight Directory Access ProtocolLDAP)的自由和开源的实现,在其OpenLDAP许可证下发行,并已经被包含在众多流行的Linux发行版中。

可以这样讲:市面上只要你能够想像得到的所有工具软件,全部都支持LDAP协议。比如说你公司要安装一个项目管理工具,那么这个工具几乎必然支持LDAP协议,你公司要安装一个bug管理工具,这工具必然也支持LDAP协议,你公司要安装一套软件版本管理工具,这工具也必然支持LDAP协议。LDAP协议的好处就是你公司的所有员工在所有这些工具里共享同一套用户名和密码,来人的时候新增一个用户就能自动访问所有系统,走人的时候一键删除就取消了他对所有系统的访问权限,这就是LDAP

有些领域并不像前端世界那么潮那么性感,但是缺了这个环节又总觉得很别扭。如果深入到运维的世界,你会发现大部分工具还活在上个世纪,产品设计完全反人类,比如cn, dc, dn, ou这样的命名方式,如果不钻研个一天两天,鬼知道它在说什么,比如说dnsdns是什么鬼?域名吗?不是,它只是某个懒惰的工程师起了dn这么一个缩写,再加一个复数,就成了dns,和域名服务器没有任何关系;cn是什么?中国的缩写?你想多了,这和中国没有任何关系。经过一系列这样疯狂的洗脑之后,你才能逐渐明白LDAP到底想干什么。抛弃你所有的认知,把自己当成一个什么都不懂的幼儿园孩子,然后我们从头学起LDAP

如果你搜索OpenLDAP的安装指南,很不幸地告诉你,网上不管中文的英文的,90%都是错的,它们都还活在上个世纪,它们会告诉你要去修改一个叫做slapd.conf的文件,基本上看到这里,你就不用往下看了,这个文件早就被抛弃,新版的OpenLDAP里根本就没有这个文件!取而代之的是slapd.d的文件夹,然后另一部分教程会告诉你,让你修改这个文件夹下的某一个ldif文件,看到这里,你也不用往下看了,你又看到了伪教程,因为这个文件夹下的所有文件的第一行都明确地写着:『这是一个自动生成的文件,不要修改它!』你修改了它之后,它的md5校验值会匹配不上,造成更多的问题。你应该用ldapmodify来修改这个文件,而关于ldapmodify的教程,可以说几乎就没有!我一开始不知道面临这样荒谬的处境,很多运维人员是怎么活下来的,不过等我自己配通了以后,真的是累到连写教程的精力都没有了,好吧,我已经配通了,你们各人自求多福吧。

架构

实际上,我的操作步骤很多都是反的,架构这部分是最后才意识到的,但实际上从最一开始就应该先想到。实际上整个OpenLDAP的架构大致包含3个部分,而网上没有教材提到这块。

OpenLDAP

首先,是OpenLDAP的服务器本身,这个东西其实只相当于是一个mysql数据库,它是没有酷炫的图形界面的,如果你愿意每次都手敲一大堆代码,也可以用它,但这种反人类的设计真的不是给人用的。

phpLDAPadmin

所以,你需要安装一个叫作phpLDAPadmin的工具,好歹这是一个图形界面,虽然奇丑无比,并且配置起来也并不容易。

PWM

光装管理工具还不够,你总要给用户提供一个修改密码的地方。

客户端

最后,你还需要配置各种工具。

架构图

我画了一个简单的架构图如下:

图片描述

安装

安装OpenLDAP

安装OpenLDAP非常简单,直接安装这3个东西就够了,甚至运气好的话,也许你的操作系统已经自带安装好了:

yum install openldap openldap-clients openldap-servers

安装完了之后可以直接启动OpenLDAP服务,不需要做任何配置,我一开始还有顾虑,后来发现完全不用多想直接启动即可:

service slapd start

配置OpenLDAP

这一块在最一开始是最麻烦的部分,网上所有教程讲的都不对。因为现在是2018年了,而很多教程还停留在2008年甚至1998年。配置OpenLDAP最正确的姿势是通过ldapmodify命令执行一系列自己写好的ldif文件,而不要修改任何OpenLDAP装好的配置文件

举个例子来说,你要想修改RootDN,那么你就自己写这么一个ldif文件,假设给它起名叫a.ldif,然后执行它就可以了:

dn: olcDatabase={2}bdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=admin,dc=qiban,dc=com
-
replace: olcSuffix
olcSuffix: dc=qiban,dc=com

怎么执行呢?

ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f a.ldif

这么长的命令是什么意思?-Q表示安静执行,-Y和后面的EXTERNAL表示,好吧,我也不知道什么意思,总之需要这样配合,然后-H表示地址,-f表示文件名。几乎所有的ldapmodify命令都这么执行就好了。

再来讲解一下上面的ldif文件的内容,你不要问为什么叫ldif这么一个破后缀,总之你记住它就是这个后缀就好了。dn表示你要修改什么东西,在这里我们用的是{2}bdb,你的系统不一定是{2}bdb,不管是几,总之你去查一下目录里的内容就好了:

ls /etc/openldap/slapd.d/cn=config/

得到的结果大概如下,不一样也不要害怕:

cn=module{0}.ldif  cn=schema/  cn=schema.ldif  olcDatabase={0}config.ldif  olcDatabase={-1}frontend.ldif  olcDatabase={1}monitor.ldif  olcDatabase={2}bdb/  olcDatabase={2}bdb.ldif

这里面有一大堆奇奇怪怪的数字,不要担心,其中有一个带什么db.ldif的就是你最终需要修改的数据库文件,我这里是bdb.ldif,你的可能是mdb.ldif,还有人是hdb.ldif,不管什么db,总之你要改的是一个叫db的文件就对了,你可以cat打开看一看,但是不要用vi去修改它。

changetype就是modify,表示我们要修改这个文件。第3行是replace,表示我们要替换里面的某个值,你可以把这个操作理解为mysql数据库的update操作,如果你把第3行改成add,那就是mysqlinsert操作了。不过这里我们操作的只是配置文件本身,还牵涉不到添加用户或者更改用户,如果你以为事情就这么简单,那就是你太天真了。

RootDN在这里就表示你整个OpenLDAP系统的管理员用户名是什么,不要奇怪,后面这一砣都是用户名cn=admin,dc=qiban,dc=com,长的有点像email地址,实际意思也差不多,但总之就不是email就行了。不要问为什么,总之cn就是email前面的那个名字,后面带dc的都是域名。

真实情况是你还需要给这个用户设置一个密码,具体怎么设自行Google,但还是那句话:不要修改系统文件,要用ldapmodify来执行。

添加memberOf模块

这个工作应该一开始就做好,要不然后面要做的话,还得把建好的组全删掉再重建。这个模块的作用是当你建一个组的时候,把一些用户添加到这个组里去,它会自动给这些用户添加一个memberOf属性,有很多应用需要检查这个属性。

添加的时候比较麻烦,需要建3ldif文件,然后1个执行ldapmodify2个执行ldapadd,错一点都不行:

memberof_config.ldif

再一次重申:文件名叫做什么根本无所谓,只要后缀名为ldif即可。

dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModuleLoad: memberof
olcModulePath: /usr/lib64/openldap

dn: olcOverlay={0}memberof,olcDatabase={2}bdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf

小心第5行和第7行,先找到你的模块目录是不是在/usr/lib64下面,然后看清楚你的数据库类型和数字,不要瞎复制。

对于这个文件,我们需要执行ldapadd

ldapadd -Q -Y EXTERNAL -H ldapi:/// -f memberof_config.ldif

执行完之后,检查你的/etc/openldap/slapd.d/cn=config/,看是不是多了一个模块,这个模块的数字编号直接影响下一步操作。

refint1.ldif

dn: cn=module{0},cn=config
add: olcmoduleload
olcmoduleload: refint

这个文件里我的memberOf是第一个模块,所以编号是0,你的不一定,要看清楚到底第几号模块是memberof,然后就改成几就可以了,对于这个文件,我们要执行ldapmodify操作:

ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f refint1.ldif

你如果能看懂它的意思的话,它的大意是说要修改我们刚刚添加的那个模块文件的内容。

refint2.ldif

dn: olcOverlay={1}refint,olcDatabase={2}bdb,cn=config
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
objectClass: top
olcOverlay: {1}refint
olcRefintAttribute: memberof member manager owner

对这个文件执行ldapadd操作:

ldapadd -Q -Y EXTERNAL -H ldapi:/// -f refint2.ldif

还是要注意检查db类型,否则你一定不能成功。

安装phpLDAPadmin

好吧,干完了上面这些啰里巴嗦的事情,你可以先给自己泡杯咖啡,接下来还有很多工作要做,不过难度已经没有刚才那么大了。

我们开始安装phpLDAPadmin

yum install phpldapadmin

CentOSyum安装总是这么令人赏心悦目。

配置phpLDAPadmin

接下来让我们在nginx里配置好它,以便让我们的管理员能够看到它。

    location /htdocs {
        alias /usr/share/phpldapadmin/htdocs;
        index index.php;
        location ~ \.php$ {
            alias /usr/share/phpldapadmin;
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }

缺省文件安装在/usr/share/phpldapadmin/htdocs下,我们必得要在这里配置一个alias才能访问到它,但在php-fpm时又要配置另外一个alias,这也是比较坑人的一个地方。

接下来你需要修改/etc/phpldapadmin/config.php这个文件,里面有大段大段的注释,看到人头晕,注意这么2点就够了,其它的都不要改:

  • $servers->setValue('login','anon_bind',false);改成false,因为我们不想让人匿名访问;
  • $servers->setValue('login','allowed_dns',array('cn=admin,dc=qiban,dc=com'));,我们只允许管理员访问,其他任何人不得访问。

使用phpLDAPadmin

你现在可以通过URL地址访问phpLDAPadmin了,登录的时候输入你那一坨用户名:cn=admin,dc=qiban,dc=com,然后输入密码,如果你前面一切都设置对了,那么这里就可以登录进去了。

clipboard.png

界面里透出一股浓浓的上世纪九十年代风格,不过好歹我们终于可以脱离纯手写代码管理的窘境了。

这时候你首先要建立两个organizationalUnit,一个叫作groups,一个叫作users。不要问为什么。

然后在users下面建几个inetOrgPerson,这些就是你的用户了。注意,在创建新条目时,一定要选择默认,不要选择什么Posix或者Generic User Account,那只会帮你建出一堆没用的Linux账号出来,我们只想要web用户,不想建什么Linux用户。注意:密码这个地方一定要选md5,否则你后面和其它系统连接会出问题。

然后在groups下面建几个组吧,比如admins, users等等,注意选择objectClassgroupOfNames就行了。然后把你刚刚建好的几个用户分门别类的给他们加到组里去。

在这一步上,如果你前面配置memberOf模块配置正确的话,你会在user显示内部属性里看到它的memberOf属性,如果看不到,说明你没有配对。

配置第三方应用

到此为止,似乎真没有什么好说的了,Phabricator, Confluence, Zabbix, Grafana, 禅道等等,几乎你能想到的任何一个第三方应用都会有说明书教你怎么配置dc, cn, ou这些东西,经过了上面这一番折腾,你怎么着也应该对LDAP的一些术语有所了解了,如果还是不行,说明你玩它的时间还是不够长,再多玩两天,也就明白了。

配置好之后的好处就是你再也不用东一块西一块地建用户了,而可以在一个统一的地方集中管理你的用户和群组授权。

结语

总之,配置OpenLDAP不是一个轻松的活,但是考虑到有那么多第三方应用都支持这个鬼东西,花点代价把它配通还是值得的。希望你一切顺利。

查看原文

赞 132 收藏 103 评论 55

em0t 赞了文章 · 2018-11-06

面试官问你如何解决web高并发这样回答就好了

所谓高并发,就是同一时间有很多流量(通常指用户)访问程序的接口、页面及其他资源,解决高并发就是当流量峰值到来时保证程序的稳定性。

我们一般用QPS(每秒查询数,又叫每秒请求数)来衡量程序的综合性能,数值越高越好,一般需要压测(ab工具)得到数据。

假设我们的一个进程(也可以是线程或者协程)处理一次请求花费了50毫秒(业内达标范围一般是20毫秒至60毫秒),那么1秒钟就可以处理20个请求,一台服务器是可以开很多这样的进程并行去处理请求的,比如开了128个,那么这台机器理论上的QPS=2560。

千万不要小瞧这个数字,当你的QPS真有这么高的时候意味着你的DAU(用户日活)有2560*200=51.2万,业内一般是放大200倍计算,有这样的日活说明做得很不错了。

一台服务器能够达到的最大QPS受很多因素的影响,比如机器参数配置、机房地理位置、CPU性能、内存大小、磁盘性能、带宽大小、程序语言、数据库性能、程序架构等,我们一一细说。

1.机器参数配置

这个很好理解,比如服务器最大可以开启128个进程,你设置了最大只开启100个,这属于服务器调优。

2.机房地理位置

如果你做海外用户,服务器机房应该选择国外的,反之应该选择国内的,因为机房距离用户越近,在传输上的时间损耗就越低。

3.CPU性能

CPU性能越好,处理速度就越快,核心数越多,能够并行开启的进程就越多。

4.内存大小

内存越大,程序就能把更多的数据直接放到内存,从内存读取数据比从磁盘读取数据的速度快很多。

5.磁盘性能

这个不用多说吧,一般固态硬盘的性能比机械硬盘的性能好很多,性能越好读写数据的速度就越快。

6.带宽大小

服务器的带宽一般指流出带宽,单位为Mb/S,比如带宽为8Mb/S即1MB/S,如果提供文件下载服务,可能一个用户的下载行为就把服务器带宽用完了。

一般把图片、视频、css文件、JavaScript脚本等资源放到第三方的CDN去,按流量计费,这样就不占用服务器带宽了。

如果用户规模小,基本上一台服务器就好了,这个时候一般会选按固定带宽大小计费。

如果用户规模很大了,基本上会用到负载均衡器来分流,即把流量按照一定的规则分配到不同的服务器上,负载均衡器一般会按流量来计费。

如果平均一次请求返回的数据大小为50KB,为了达到1000QPS这个指标,需要的带宽峰值=1000*50*8/1024=390.625Mb/S。

我们在设计接口的时候应该尽量减少返回的数据大小,比如user_id就可以简化为uid,像图片、视频、css等文件压缩的目的就是减少数据的大小。

7.程序语言

编译型语言的性能一般好于解释型语言的性能,比如go语言性能就好于php语言性能,当语言短期不会替换时,可以通过堆机器解决高并发问题。

8.数据库性能

一台服务器上部署的数据库总是有一个瓶颈的,比如每秒查询数、每秒写入数。

我们可以通过增加很多从库解决查询(select语句)的瓶颈,称之为多从库模型,需要注意的是主从同步数据可能有延迟,当修改数据后马上需要查询时需要设置强制从主库读取。

我们可以将业务拆分,让某些表存储在一个数据库实例上,另一些表存储在其他数据库实例上,虽然一个数据库实例有自己的瓶颈,但是很多的数据库实例堆积起来性能就会大大改善,多个数据库实例的方案称之为多主库模型,主要是为了解决写入瓶颈(insert语句、update语句、delete语句)。

如果你有多个主库又有多个从库,你就实现了多主多从模型。

如果一个表存储的数据量很大,这个时候就要考虑分表了(一般用中间件实现),比如按时间分表或者按用户分表,当把一个表的所有分表都放在一个数据库实例上都满足不了要求的时候,你应该把某些分表存储在新的数据库实例上,这个时候一个表的数据分布到了不同的数据库实例上,这就是所谓的分布式数据库方案了,你需要处理的事情就很复杂了,比如处理分布式事务。

数据库的并发连接数也是有限制的,我们可以用连接池技术来应对,就是保持一定数量的和数据库的连接不断开的长连接,需要连接数据库的时候就从池子里选择一个连接,用完放回去就好了,这个一般也是用中间件来实现。

好的索引也能提高数据库的性能,有时候比堆多个从库的方案还要好。

如果能够减少数据库的读写,也算间接提高了数据库的性能,比如我们用redis来做缓存,用消息队列异步落库等。

有时候某些数据用数据库来计算需要很长时间,可以取到元数据(最小粒度的数据)用程序来计算,这称之为用内存换时间。

9.程序架构

比如实现同样的功能,初级程序员写的程序需要循环100次,而高级程序员写的程序只需要循环10次,效果肯定不一样。

总结

一般大型项目基本是前后端分离的,从性能方面说就是为了将页面渲染的处理在客户端运行,降低服务器的压力。

从带宽层面考虑,css、图片、视频、JavaScript等文件资源能用CDN的就用CDN,能压缩的就尽量压缩,接口能减小返回数据的大小就尽量减小。

为了解决编程语言的不足或者单台服务器的瓶颈,可以先堆机器应对。

索引、多主多从、分布式数据库、缓存、连接池、消息队列等是从数据库方便考虑如何优化性能。

有时候程序的低耦合性比程序的高性能更重要,不要一味地追求高性能。

TODO

持续更新

查看原文

赞 48 收藏 36 评论 5

em0t 赞了回答 · 2018-10-26

解决php类名大小写问题

1.php 中
类,方法名,函数名不区分大小写
变量,常量区分大小写
2.win下目录不区分大小写, Linux 下目录区分

因此检查下你的代码,是否有目录名称
ps:强烈建议,统一大小写,不要随性,不然小心被人打死????

关注 8 回答 7

em0t 赞了文章 · 2018-10-10

[进阶篇]docker编排PHP开发坏境

图片描述

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。容器技术是继大数据和云计算之后又一炙手可热的技术,而且未来相当一段时间内都会非常流行。

概述

由于本人常用Windows电脑就索性就选择win10系统来使用docker,由于docker在win10以下运行需要安装虚拟机,果断的升级到win0专业版,win10所提供的Hyper-V服务(目前仅专业版、企业版支持)完美解决了运行虚拟机各种卡的问题。

  • 本文主要使用docker-compose编排PHP基本环境:

    • nginx
    • PHP
    • MySQL
    • Redis
    • MySQL在线管理
    • Redis在线管理
适用于Windows、Linux、MacOs部署PHP运行环境

结构简介

图片描述

目录结构

├── conf                    配置文件目录
│   ├── conf.d              Nginx用户站点配置目录
│   ├── nginx.conf          Nginx默认配置文件
│   ├── mysql.cnf           MySQL用户配置文件
│   ├── php-fpm.conf        PHP-FPM配置文件(部分会覆盖php.ini配置)
│   └── php.ini             PHP默认配置文件
├── docker-compose.yml      PHP最新版docker-compose项目文件
├── log                     Nginx日志目录
├── mysql                   MySQL数据目录8.0
├── php                     PHP7.2
└── www                     PHP代码目录
使用前需要搭建 git、docker、docker-compose

使用方法

  1. clone项目

    $ git clone https://github.com/LoyaltyLu/docker.git
  2. Linux下需要将当前用户加入docker用户组

    $ sudo gpasswd -a ${USER} docker
  3. 启动环境

    $ cd dockerphp
    $ docker-compose up
  4. 初次启动需要初始化镜像编译环境所以时间稍微长点就要看服务器或自己网速
  5. 安装成功后访问localhost

clipboard.png

容器基本使用介绍

容器使用networks方式进行通信

日志查看

log文件生成位置可在conf下配置文件中自行更改,更改时需要相对应更改docker-compose对应映射目录,以便对日志数据持久化存储

composer

在PHP容器中默认安装composer进入容器后即可使用。

  • 容器进入:

    $ docker exec -it <容器名> /bin/bash

phpMyAdmin

phpMyAdmin 暴露端口:8080

phpRedisAdmin

phpRedisAdmin 暴露端口:8081

使用XDEBUG调试

环境中安装XDEBUG了调试,需对php.ini进行配置后启用,配置如下:

[XDebug]
xdebug.remote_enable = 1
xdebug.remote_handler = "dbgp"
xdebug.remote_host = "172.17.0.1"
xdebug.remote_port = 9000
xdebug.remote_log = "/var/log/dnmp/php.xdebug.log"

配置完成后需要重启下PHP容器。

查看原文

赞 40 收藏 32 评论 7

em0t 赞了文章 · 2018-10-10

Docker容器技术实战

docker基础

linux namespace机制可参考:http://www.infoq.com/cn/artic...
linux cgroups机制可参考:http://www.infoq.com/cn/artic...

docker服务安装

宿主机系统与docker版本选型

从现有的系统来看centos7对虚拟化的支持相对来说更完美,对于docker的版本当然是最新的版本功能更完善;故选择 Centos7+docker最新版本

kernel升级

现象描述:系统死机
原因描述:centos7.1默认安装的内核为3.10版本,对于运行少量的容器来说不存在任何问题,当运行容器的数量增多时(比如100台)就会发现容器运行一段时间后操作系统会死机,导致整个物理机不可用,经研究发现将系统内核升级到4.2即可解决此问题。
解决方案:
查看系统当前内核版本

[root@docker6 ~]# uname -a
Linux docker6.stg.1qianbao.com 3.10.0-327.el7.x86_64 #1 SMP Thu Nov 19 22:10:57 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

升级kernel至4.2

rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
yum install http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install kernel-ml
grub2-set-default 0

重启操作系统

reboot

再次查看kernel版本

[root@docker6 ~]# uname -a
Linux docker6 4.2.1-1.el7.elrepo.x86_64 #1 SMP Mon Sep 21 20:01:19 EDT 2015 x86_64 x86_64 x86_64 GNU/Linux

如需卸载可使用

yum remove kernel-ml

安装docker

配置yum源

[root@docker6 ~]# vim /etc/yum.repos.d/docker.repo
[docker-epel]
name=docker-epel
baseurl=http://mirrors.aliyun.com/centos/$releasever/extras/$basearch/
gpgcheck=0

亦可下载相关rpm包至服务器安装。

安装docker

[root@docker6 ~]# yum install docker -y
Loaded plugins: fastestmirror
Repository 'moosefs' is missing name in configuration, using id
Loading mirror speeds from cached hostfile
Resolving Dependencies
--> Running transaction check
.......
Complete!

启动docker服务

[root@docker6 ~]# systemctl start docker.service

查看docker版本

[root@docker6 ~]# docker -v
Docker version 1.7.1, build 786b29d/1.7.1

拉取docker镜像

本处使用自定仓储,使用时请根据实际情况修改

搜索仓储里面的镜像

如果仓储不是https协议需在docker服务参数中使用--insecure-registry=xxoo.com添加至信任列表,并重启服务

[root@docker6 ~]# docker search 1qianbao.com/jboss
INDEX          NAME                                        DESCRIPTION   STARS     OFFICIAL   AUTOMATED
1qianbao.com   1qianbao.com/library/centos6.5_jboss                      0                    
1qianbao.com   1qianbao.com/library/centos6.5_jboss_base                 0                    
1qianbao.com   1qianbao.com/library/jboss                                0                    
1qianbao.com   1qianbao.com/library/jboss_logstash                       0 

拉取到本地

[root@docker6 ~]# docker pull 1qianbao.com/jboss
Using default tag: latest
512b1dcc52f6: Download complete 
0757207a8bea: Download complete 
bf839298b71b: Download complete 
15661f85a6fd: Download complete 
d4f8087e2cbc: Download complete 
7aebe5484289: Download complete 
35bd70dfe627: Download complete 
070eee356b7c: Download complete 
6559d408744a: Download complete 
d9406fd1f731: Download complete 
Status: Downloaded newer image for 1qianbao.com/jboss:latest

查看本地镜像

[root@docker6 ~]# docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
1qianbao.com/jboss   latest              512b1dcc52f6        7 weeks ago         832.8 MB

创建一个容器

[root@docker6 ~]# docker run -id -h test_hostname --name test_conftainer_name 1qianbao.com/jboss /bin/bash
2744cb820ec0b2d8a80d347df3d6631703822841066954ad1dbd3ba779fb09c2
[root@docker6 ~]# docker ps -a
CONTAINER ID        IMAGE                COMMAND             CREATED             STATUS              PORTS               NAMES
2744cb820ec0        1qianbao.com/jboss   "/bin/bash"         59 seconds ago      Up 58 seconds       22/tcp              test_conftainer_name
[root@docker6 ~]# docker exec test_conftainer_name whoami
root

查看容器信息

[root@docker6 ~]# docker inspect test_conftainer_name
[
{
    "Id": "2744cb820ec0b2d8a80d347df3d6631703822841066954ad1dbd3ba779fb09c2",
    "Created": "2017-03-09T08:18:35.542367209Z",
    "Path": "/bin/bash",
    "Args": [],
    "State": {
        "Running": true,
        "Paused": false,
        "Restarting": false,
        "OOMKilled": false,
        "Dead": false,
        "Pid": 41307,
        "ExitCode": 0,
        "Error": "",
        "StartedAt": "2017-03-09T08:18:35.942277675Z",
        "FinishedAt": "0001-01-01T00:00:00Z"
    },
    "Image": "512b1dcc52f663dcc136570f64793fa1a7f4dbb69024e4704a055ef90104ec6f",
    .......
    "HostConfig": {
        "Binds": null,
        "ContainerIDFile": "",
        "LxcConf": [],
        "Memory": 0,
        "MemorySwap": 0,
        "CpuShares": 0,
        "CpuPeriod": 0,
        "CpusetCpus": "",
     .......
]

自定义服务启动参数

修改配置文件

[root@docker4 ~]# cat /etc/sysconfig/docker
# /etc/sysconfig/docker
# Modify these options if you want to change the way the docker daemon runs
OPTIONS='--selinux-enabled=false --storage-driver=devicemapper --storage-opt dm.basesize=100G --insecure-registry=1qianbao.com -b br0 -H unix:///var/run/docker.sock -H tcp://0.0.0.0:9999'

参数详解

--storage-driver=devicemapper            #使用devicemapper方式驱动,驱动模式详细介绍可参照[http://blog.csdn.net/qq_26923057/article/details/52351731][3]
--storage-opt dm.basesize=100G           #为每个容器分配100G磁盘空间
--selinux-enabled=false                  #容器内禁用selinux
--insecure-registry=1qianbao.com         #指定http协议访问的仓储地址
-H unix:///var/run/docker.sock           #指定docker服务的socket文件位置,可供其它应用通过socket形式访问
-H tcp://0.0.0.0:9999                    #指定docker服务remote api端口,可通过docker-py远程连接访问
-b br0                                   #容器网络使用桥接形式,桥接至本机br0网卡

修改完毕后,重启docker服务即可

容器资源限制

内存限制

-m, --memory=""                    #限制容器使用的物理内存,单位可以为k, m, g等
--memory-swap=""                   #限制容器物理内存+swap缓存总和的大小,单位可以为k, m, g等

实例:


[root@docker6 ~]# docker run -id -m 4g --memory-swap=4g -h test_hostname --name test_conftainer_name 1qianbao.com/jboss /bin/bash
2b4dbf2a3d2a146d2958210b783ad7a4fee413c0399757d43464a8a9802bb446
[root@docker6 ~]# docker inspect test_conftainer_name | grep-i 'mem'
        "Memory": 4294967296,
        "MemorySwap": 4294967296,
        "CpusetMems": "",
        "MemorySwappiness": -1,

CPU限制

--cpu-shares=0            #设置CPU利用率权重,默认为1024
--cpu-period=0            #限制容器的CPU使用周期。一般--cpu-period配合--cpu-quota一起使用。例如cpu-period为100ms,cpu-quota为200ms,表示最多可以使用2个cpu;默认值为100ms
--cpuset-cpus=""          #绑定指定容器使用指定CPU,默认使用所有cpu核心
--cpu-quota=0             #与--cpu-period配合使用

实例:

[root@docker6 ~]# docker run -id --cpu-shares=100 --cpu-period=100000 --cpu-quota=200000 -h test_hostname_cpu --name test_conftainer_cpu 1qianbao.com/jboss /bin/bash
8f1894bf1550f929d10164b3f538af66d7e5922611b58556f5baeea91dfd1c97
[root@docker6 ~]# docker inspect test_conftainer_cpu | grep -i cpu
    "Name": "/test_conftainer_cpu",
        "CpuShares": 100,
        "CpuPeriod": 100000,
        "CpusetCpus": "",
        "CpusetMems": "",
        "CpuQuota": 200000,
        "Hostname": "test_hostname_cpu",
[root@docker6 ~]# docker exec -it test_conftainer_cpu /bin/bash        
[root@test_hostname_cpu /]# cat high_cpu.sh 
#!/bin/bash

while :
do
    let '1+1'
done
[root@test_hostname_cpu /]# bash high_cpu.sh &
[1] 2105
[root@test_hostname_cpu /]# bash high_cpu.sh &
[2] 2106
[root@test_hostname_cpu /]# bash high_cpu.sh &
[3] 2107
[root@test_hostname_cpu /]# ps -ef | grep high_cpu
root       2105     18 70 17:08 ?        00:00:22 bash high_cpu.sh
root       2106     18 67 17:08 ?        00:00:20 bash high_cpu.sh
root       2107     18 68 17:08 ?        00:00:19 bash high_cpu.sh
root       2111     18  0 17:08 ?        00:00:00 grep high_cpu
[root@docker6 ~]# docker stats test_conftainer_cpu
CONTAINER             CPU %               MEM USAGE/LIMIT     MEM %               NET I/O

test_conftainer_cpu 200.04% 15.09 MB/270.4 GB 0.01% 648 B/648 B

服务调优

内核调优

现象描述:网络高延时、丢包
原因描述:单台宿主机运行百台以上容器时,对宿主机的压力相应增加,其中网络方面表现最为明显;
以ARP为例:容器使用br0的网络与外面通信,这样每台容器都会将arp信息缓存下来,如果100台容器分别将网关、宿主机这两条信息存入ARP缓存表,那么物理机上的arp缓存表就有200条;而系统默认的arp缓存条目为128条,所以将导致一部分arp信息被覆盖掉。如果容器间进行频繁的网络通信,就会表现出大量的延时甚至丢包。

[root@docker6 ~]# cat /proc/sys/net/ipv4/neigh/default/gc_thresh1    #系统默认的ARP缓存条目
128
[root@docker6 ~]# cat /proc/sys/net/ipv4/neigh/default/gc_thresh2    #系统ARP条目值的软限制
512
[root@docker6 ~]# cat /proc/sys/net/ipv4/neigh/default/gc_thresh3    #系统ARP条目值的硬限制
1024

解决方案:
修改系统内核参数

[root@docker6 ~]# vim /etc/sysctl.conf
# System default settings live in /usr/lib/sysctl.d/00-system.conf.
# To override those settings, enter new settings here, or in an /etc/sysctl.d/<name>.conf file
#
# For more information, see sysctl.conf(5) and sysctl.d(5).

net.ipv4.ip_forward=1
net.ipv4.neigh.default.gc_thresh1=25600
net.ipv4.neigh.default.gc_thresh2=51200
net.ipv4.neigh.default.gc_thresh3=102400
net.core.rmem_default = 2097152
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.core.wmem_default = 2097152
net.ipv4.neigh.default.base_reachable_time = 172800
net.ipv4.neigh.default.gc_stale_time = 172800
kernel.msgmax=65536
kernel.msgmnb=65536

重启或执行如下命令使之生效

[root@docker6 ~]# sysctl -p

ulimit调优

现象描述:容器启动服务时资源耗尽
原因描述:大宿主机情况下,多个容器并行启动多进程或多线程服务时大量消耗文件句柄导致资源耗尽。
解决方案:修改系统对普通用户的nproc限制

[root@docker6 ~]# cat /etc/security/limits.d/20-nproc.conf 
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.

#*          soft    nproc     4096
root       soft    nproc     unlimited

检测宿主机最大打开的句柄数可使用:for i in {1..200000};do sleep 2000 & done;

data资源池调优

docker服务启动的时候默认会创建一个107.4G的data文件,而后启动的容器的所有更改内容全部存储至这个data文件中;也就是说当容器内产生的相关data数据超过107.4G后容器就再也没有多余的空间可用,从而导致所有容器的根目录变为只读!

宿主机的docker info信息如下

[root@docker6 ~]# docker info
 Data file: /dev/loop0
 Data Space Used: 2.749 GB
 Data Space Total: 107.4 GB
 Data Space Available: 104.6 GB
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data

解决方案:使用磁盘分区、lvs逻辑卷、虚拟大文件替换data池文件
本例中使用虚拟大文件替换data池文件,步骤如下:

[root@docker6 ~]# cat init_docker.sh 
#!/bin/bash
echo "stop docker service"
service docker stop
echo "delete the docker data pool"
/bin/rm -rf /var/lib/docker/
echo "create docker data pool"
mkdir -p /var/lib/docker/devicemapper/devicemapper/
dd if=/dev/zero bs=1G count=0 of=/var/lib/docker/devicemapper/devicemapper/data seek=1500
dd if=/dev/zero bs=1G count=0 of=/var/lib/docker/devicemapper/devicemapper/metadata seek=2
#clear veth net
ifconfig | grep veth | awk -F ':' '{print $1}' | while read LINE;do ip link delete $LINE;done
echo "start docker service"
service docker start

[root@docker6 ~]# bash init_docker.sh 

执行上述命令后,docker info信息如下

[root@docker6 ~]# docker info | grep Data
 Data file: /dev/loop2
 Data Space Used: 1.821 GB
 Data Space Total: 1.611 TB
 Data Space Available: 1.491 TB
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data

制作镜像文件

基于YUM源制作镜像

如要制作centos6版本镜像,宿主机必须也为centos6系统;同理制作centos7惊险需在centos7版本宿主机上执行

制作镜像脚本如下:

[root@testmanager ~]# cat create_base_container.sh
#!/bin/bash
yum_config=/etc/yum.conf
if [ -f /etc/dnf/dnf.conf ] && command -v dnf &> /dev/null; then
    yum_config=/etc/dnf/dnf.conf
    alias yum=dnf
fi
target=$(mktemp -d --tmpdir $(basename $0).XXXXXX)
set -x
mkdir -m 755 "$target"/dev
mknod -m 600 "$target"/dev/console c 5 1
mknod -m 600 "$target"/dev/initctl p
mknod -m 666 "$target"/dev/full c 1 7
mknod -m 666 "$target"/dev/null c 1 3
mknod -m 666 "$target"/dev/ptmx c 5 2
mknod -m 666 "$target"/dev/random c 1 8
mknod -m 666 "$target"/dev/tty c 5 0
mknod -m 666 "$target"/dev/tty0 c 4 0
mknod -m 666 "$target"/dev/urandom c 1 9
mknod -m 666 "$target"/dev/zero c 1 5
if [ -d /etc/yum/vars ]; then
    mkdir -p -m 755 "$target"/etc/yum
    cp -a /etc/yum/vars "$target"/etc/yum/
fi 
function clear_repo(){
    rm -f "$target"/etc/yum.repos.d/*
    cp /etc/yum.repos.d/*.repo "$target"/etc/yum.repos.d/
}
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y groupinstall 'Core'
clear_repo
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y groupinstall 'Base'
clear_repo
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y groupinstall 'Chinese Support'
clear_repo
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y install tar bind-utils wget man sysstat pexpect vim telnet net-tools mysql unzip zip tcpdump lrzsz openssh-clients expect pexpect
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y reinstall rpm-libs
yum -c "$yum_config" --installroot="$target" --releasever=/ --setopt=tsflags=nodocs --setopt=group_package_types=mandatory -y clean all
cat > "$target"/etc/sysconfig/network <<EOF
NETWORKING=yes
HOSTNAME=localhost.localdomain
EOF
cat > "$target"/etc/sysconfig/network-scripts/ifcfg-eth0 <<EOF
DEVICE=eth0
ONBOOT=yes
BOOTPROTO=none
TYPE=Ethernet
EOF
/bin/cp "$target"/usr/share/zoneinfo/Asia/Shanghai "$target"/etc/localtime
/bin/rm -f "$target"/var/lib/rpm/__db*
sed -i '/1024/d' "$target"/etc/security/limits.d/90-nproc.conf
echo 'ZONE="Asia/Shanghai"' > "$target"/etc/sysconfig/clock
echo 'UTC=False' >> "$target"/etc/sysconfig/clock 
#root password is uplooking
password='$6$ZTypyZdt$0.qmfnMd3W5bCFfXjJ2FPPDxxZBVrxd5WYFeEyYfIEwyRXrQtHHztQdNwWjEwCiCPxiTzuTJmns3G7R5q5Y1W.'
sed -i "s#root:\*#root:${password}#" "$target"/etc/shadow
sed -i 's/#UseDNS yes/UseDNS no/' "$target"/etc/ssh/sshd_config
for file in "$target"/etc/{redhat,system}-release
do
    if [ -r "$file" ]; then
        version="$(sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' "$file")"
        break
    fi
done
version=latest
if [ -z "$version" ]; then
    echo >&2 "warning: cannot autodetect OS version, using '$name' as tag"
    version=$name
fi
tar --numeric-owner -c -C "$target" . | docker import - $name:$version
docker run -i -t --rm $name:$version /bin/bash -c 'echo success'
rm -rf "$target"

执行结果如下:

[root@testmanager ~]# bash create_base_container.sh centos6.5_base_image
+ mkdir -m 755 /tmp/create_base_container.sh.vVacHy/dev
+ mknod -m 600 /tmp/create_base_container.sh.vVacHy/dev/console c 5 1
+ mknod -m 600 /tmp/create_base_container.sh.vVacHy/dev/initctl p
+ mknod -m 666 /tmp/create_base_container.sh.vVacHy/dev/full c 1 7
+ mknod -m 666 /tmp/create_base_container.sh.vVacHy/dev/null c 1 3
+ mknod -m 666 /tmp/create_base_container.sh.vVacHy/dev/ptmx c 5 2
........
+ '[' -z latest ']'
+ tar --numeric-owner -c -C /tmp/create_base_container.sh.vVacHy .
+ docker import - centos6.5_base_image:latest
94b212003e6acd1754442a7ce93279a8654c3122d46bbe79fd4ab077ebb1335f
+ docker run -i -t --rm centos6.5_base_image:latest /bin/bash -c 'echo success'
success
+ rm -rf /tmp/create_base_container.sh.vVacHy

查看制作的镜像:

[root@testmanager ~]# docker images
REPOSITORY                                              TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
centos6.5_base_image                                    latest              94b212003e6a        4 minutes ago       438.6 MB

基于Dockerfile制作镜像

以制作nginx镜像为例:

编写nginx启动脚本

[root@testmanager docker_build_for_nginx]# cat start_nginx
#!/bin/bash

service nginx restart

/bin/bash

编写Dockerfile文件

[root@testmanager docker_build_for_nginx]# cat Dockerfile 
FROM centos6.5_base_image
MAINTAINER shenpeng.roc@gmail.com
RUN yum install nginx -y
ADD start_nginx.sh /root/start_nginx.sh
EXPOSE 80
CMD ["/bin/bash /root/start_nginx.sh"]

制作镜像:

[root@testmanager docker_build_for_nginx]# docker build -t '1qianbao.com/nginx_test' .
Sending build context to Docker daemon 3.072 kB
Sending build context to Docker daemon 
Step 0 : FROM centos6.5_base_image
 ---> 94b212003e6a
Step 1 : MAINTAINER shenpeng.roc@gmail.com
 ---> Running in 85d4b56471c5
 ---> c2a862a03b91
Removing intermediate container 85d4b56471c5
Step 2 : RUN yum install nginx -y
 ---> Running in 06864f81b09b
Loaded plugins: fastestmirror
Setting up Install Process
Resolving Dependencies
--> Running transaction check
........
Complete!
 ---> 2f7c48ade298
Removing intermediate container 06864f81b09b
Step 3 : ADD start_nginx.sh /root/start_nginx.sh
 ---> e2934b310453
Removing intermediate container e52c91bc10cb
Step 4 : EXPOSE 80
 ---> Running in 23722048bfd2
 ---> 44b7299c44f9
Removing intermediate container 23722048bfd2
Step 5 : CMD /bin/bash /root/start_nginx.sh
 ---> Running in dd73d04d5e66
 ---> f90eb2c8f500
Removing intermediate container dd73d04d5e66
Successfully built f90eb2c8f500

搭建registry仓储

安装registry软件包

[root@docker6 ~]# yum install docker-registry -y

仓储默认使用5000端口,配置项为REGISTRY_PORT,配置文件内容如下:

[root@docker6 ~]# cat /etc/sysconfig/docker-registry 
# The Docker registry configuration file
# DOCKER_REGISTRY_CONFIG=/etc/docker-registry.yml

# The configuration to use from DOCKER_REGISTRY_CONFIG file
SETTINGS_FLAVOR=local

# Address to bind the registry to
REGISTRY_ADDRESS=0.0.0.0

# Port to bind the registry to
REGISTRY_PORT=5000

# Number of workers to handle the connections
GUNICORN_WORKERS=8

启动仓储服务

[root@docker6 ~]# service docker-registry start
[root@docker6 ~]# netstat -antp | grep 5000
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      45148/python

上传镜像至仓储

docker-registry默认启动http协议,故需使用--insecure-registry=127.0.0.1:5000选项添加信任;我的仓储搭建在本地所以使用127.0.0.1,可跟进实际情况修改,亦可使用域名

[root@docker6 ~]# docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
1qianbao.com/jboss   latest              512b1dcc52f6        7 weeks ago         832.8 MB
[root@docker6 ~]# docker tag 1qianbao.com/jboss 127.0.0.1:5000/test_jboss
[root@docker6 ~]# docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
1qianbao.com/jboss          latest              512b1dcc52f6        7 weeks ago         832.8 MB
127.0.0.1:5000/test_jboss   latest              512b1dcc52f6        7 weeks ago         832.8 MB
[root@docker6 ~]# docker push 127.0.0.1:5000/test_jboss
The push refers to a repository [127.0.0.1:5000/test_jboss] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/test_jboss (1 tags)
0757207a8bea: Image successfully pushed 
bf839298b71b: Image successfully pushed 
15661f85a6fd: Image successfully pushed 
d4f8087e2cbc: Image successfully pushed 
7aebe5484289: Image successfully pushed 
35bd70dfe627: Image successfully pushed 
070eee356b7c: Image successfully pushed 
6559d408744a: Image successfully pushed 
d9406fd1f731: Image successfully pushed 
512b1dcc52f6: Image successfully pushed 
Pushing tag for rev [512b1dcc52f6] on {http://127.0.0.1:5000/v1/repositories/test_jboss/tags/latest}

从仓储拉取镜像:
先删除所有容器及镜像

[root@docker6 ~]# docker rmi 127.0.0.1:5000/test_jboss
Untagged: 127.0.0.1:5000/test_jboss:latest
[root@docker6 ~]# docker rmi 1qianbao.com/jboss
Untagged: 1qianbao.com/jboss:latest
Deleted: 512b1dcc52f663dcc136570f64793fa1a7f4dbb69024e4704a055ef90104ec6f
Deleted: d9406fd1f7318cd15f0248f8d72dc7945035848867c49350880aaf0b725138a9
Deleted: 6559d408744a0044acb96c6847016f1c2a49d9431b392d6aee10910d33643f81
Deleted: 070eee356b7cdcbdaf894ac47d1587593222e837476b7154a923cc413f8ddc76
Deleted: 35bd70dfe627069114ac98cdd495ddf674f1fc5ddad4318e72f9572475a60c8c
Deleted: 7aebe5484289372c37edf104f26ed76040fa52160b8beb15d3bfdc071383ea13
Deleted: d4f8087e2cbcafff60f4a6bb4f9612c094605cfa2999175b8efb26236d8b7d0a
Deleted: 15661f85a6fd2f7429384634a67a4c317e0474fde7f38e64eef7e28075dbc411
Deleted: bf839298b71b6b9530b992adaba8103e0ca19a0988deb76ea6a1c7e42f980e31
Deleted: 0757207a8bea0e10d2653d1e47af2f5c217e76623cd7ba827ff7747b97b9d3c7
[root@docker6 ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
[root@docker6 ~]# docker rmi 1qianbao.com/jboss
Untagged: 1qianbao.com/jboss:latest
Deleted: 512b1dcc52f663dcc136570f64793fa1a7f4dbb69024e4704a055ef90104ec6f
Deleted: d9406fd1f7318cd15f0248f8d72dc7945035848867c49350880aaf0b725138a9
Deleted: 6559d408744a0044acb96c6847016f1c2a49d9431b392d6aee10910d33643f81
Deleted: 070eee356b7cdcbdaf894ac47d1587593222e837476b7154a923cc413f8ddc76
Deleted: 35bd70dfe627069114ac98cdd495ddf674f1fc5ddad4318e72f9572475a60c8c
Deleted: 7aebe5484289372c37edf104f26ed76040fa52160b8beb15d3bfdc071383ea13
Deleted: d4f8087e2cbcafff60f4a6bb4f9612c094605cfa2999175b8efb26236d8b7d0a
Deleted: 15661f85a6fd2f7429384634a67a4c317e0474fde7f38e64eef7e28075dbc411
Deleted: bf839298b71b6b9530b992adaba8103e0ca19a0988deb76ea6a1c7e42f980e31
Deleted: 0757207a8bea0e10d2653d1e47af2f5c217e76623cd7ba827ff7747b97b9d3c7
[root@docker6 ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
[root@docker6 ~]# docker pull 127.0.0.1:5000/test_jboss
Using default tag: latest
512b1dcc52f6: Pulling dependent layers 
0757207a8bea: Downloading [======================>                            ] 64.03 MB/142.8 MB
0757207a8bea: Pulling metadata 
......

如需使用TLS访问仓储可使用nginx在前端做一层代理

docker swarm集群

swarm集群详细文档请参照:https://docs.docker.com/engin...

查看原文

赞 5 收藏 9 评论 1

em0t 赞了文章 · 2018-10-10

如何安全便捷地管理Docker船队

Docker

Docker是什么?如果说你的服务器是一条船,你只是一个小船主,你的船上散装着各种货物(也就是服务,比如http服务,数据库服务,缓存服务,消息服务等等),那么Docker就相当于把你的服务器改装成了一条集装箱货船,把你原先凌乱堆放的货物放置在一个一个容器里,互相隔离,有序堆放。我们来看看Docker的商标,是不是很形象呢?

Docker_logo_011.0.png

但是你的业务越做越大,很明显一条船是装不下了,你需要一支船队:

图片描述

这时候,如何管理船队就变成了一个难题,没有工具的帮忙,你甚至都不知道你有几条船,每条船上装的都是些什么货物,这些货物现在的状态如何。

Portainer

Portainer这个工具就是管理你船队的一个好帮手。并且它本身也是安装在一条船上,这条船就是你的指挥艇吧(说实话这个图标有点丑,一点都没有船队老大的气质)。

图片描述

怎么安装Portainer呢?这个别人早都已经介绍过了,包括官网上也有说明。但是我要介绍一下我的经验,我安装Portainer的方式和官网的也不同,和别人讲的也不同:

docker run -d --network=host --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

在这里我们没有暴露9000端口,而是使用了一个特殊的设置:--network=host,就是这一点点小小的不同,会对你后面的操作影响巨大。

Docker API

光装上Portainer是没有用的,它充其量也只能管理它所在的这条船。

它怎么管理整个船队呢?首先你得建立Portainer和船队里其它船只之间的联系。这个联系就是让被管理的船只暴露出Docker API接口来。但是如果你查看Docker官方的说明文档,把整个过程弄得极其复杂无比,又要建立什么安全证书中心,又要颁发证书,没有初中以上文化是搞不定的。

Docker本来是有一套简便的暴露端口的方法的,为什么官网要搞这么复杂呢?因为原先的方法比较简单,会直接把整个管理界面全部暴露给公网,有极大的安全风险,所以Docker官网搞了一套复杂的认证流程。

我们的解决思路比较简单:你不要把端口暴露给公网不就行了吗?加一个防火墙,只让我们允许的地址来访问就可以了,何必搞什么CA认证!

回到我们的被管理的船上,把API端口先暴露出来:

# systemctl edit docker
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375

好吧,这只是CentOS 7下的一种作法,还有一种作法是修改/etc/sysconfig/docker文件:

OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375'

具体应该用哪种作法,要取决于你的docker服务是怎么设置的:

systemctl show docker | grep EnvironmentFile

你如果有EnvironmentFile设定,那么你就需要用上面第二种办法,如果没有,就用第一种办法。

iptables

好了,现在端口是暴露出来了,但是全公网任何人都能访问,安全性怎么办?这时候我们发现一个严重的问题:Docker在乱搞我们的iptables表!

iptables是很重要的防火墙的设置,docker为了暴露它的服务,它会忽视你设置在iptables表里的一切规则,强行让它的规则生效。这还了得?我们必须禁止Docker这么胡搞,在上面的options里再加上一个选项--iptables=false

OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375' --iptables=false

重启Docker服务后,它终于不再乱动我们的iptables表了。实际上为了安全起见,我们应该在所有docker服务器上都加上--iptables=false的选项。但是这样又产生了另外一个问题:外部访问固然是阻止了,但我们的Docker容器想访问外部也访问不了了,比如我们一开始安装的Portainer也是运行在容器里的,不能访问互联网,它就没有办法管理其它Docker服务器了。为了解决这个问题,需要我们在建立Docker容器的时候指定--network=host,这也就是本文一开始安装Portainer时候那样设置的原因。

好,现在各个被管理端的2375端口是开开了,但是除了localhost谁也访问不了它,怎么办?我们可以在iptables里增加一条规则:

-A INPUT -s ###.###.###.### -m state --state NEW -m tcp -p tcp --dport 2375 -j ACCEPT

上面那个###.###.###.###就是你的Portainer指挥艇服务器的IP地址。这样一来,除了这台指挥艇服务器可以访问被管理船只的Docker API接口以外,别的任何人都不能访问。这样就既达到了我们管理的目的,又保证了安全,同时还免出去了设置证书的繁琐。

启航吧,船队!

图片描述

查看原文

赞 107 收藏 85 评论 1

em0t 赞了文章 · 2018-10-09

深入理解PHP之isset和array_key_exists对比

1、概述

经常使用isset判断变量或数组中的键是否存在, 但是数组中可以使用array_key_exists这个函数, 那么这两个谁最优呢?

官方文档对两者的定义

-分类描述文档
isset语言构造器检测变量是否已设置并且非 NULLhttp://php.net/manual/zh/function.isset.php
array_key_exists函数检查数组里是否有指定的键名或索引http://php.net/manual/zh/function.array-key-exists.php
isset() 对于数组中为 NULL 的值不会返回 TRUE,而 array_key_exists() 会。
array_key_exists() 仅仅搜索第一维的键。 多维数组里嵌套的键不会被搜索到。
要检查对象是否有某个属性,应该去用 property_exists()

2、测试

2.1 测试环境

OSPHPPHPUnit
MacOS 10.13.6PHP 7.2.7 (cli)PHPUnit 6.5.7

2.2 单元测试

class issetTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @dataProvider dataArr
     */
    public function testName($arr)
    {
        $this->assertTrue(isset($arr['name']));
        $this->assertFalse(isset($arr['age']));
        $this->assertTrue(isset($arr['sex']));
        $this->assertTrue(array_key_exists('name', $arr));
        $this->assertTrue(array_key_exists('age', $arr));
        $this->assertTrue(array_key_exists('sex', $arr));
        $this->assertFalse(empty($arr['name']));
        $this->assertTrue(empty($arr['age']));
        $this->assertTrue(empty($arr['sex']));
    }

    public function dataArr()
    {
        return [
            [
                ['name' => 123, 'age' => null, 'sex' => 0]
            ]
        ];
    }
}

/*
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 113 ms, Memory: 8.00MB

OK (1 test, 9 assertions)
*/

2.3 性能-执行时间

如上, php cli环境下, 执行10000000次, 测试代码和执行时间如下:

<?php

$arr = [
    'name' => 123,
    'age' => null
];

$max = 10000000;

testFunc($arr, 'name', $max);
testFunc($arr, 'age', $max);

function testFunc($arr, $key, $max = 1000)
{
    echo '`$arr[\'', $key, '\']` | - | -', PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        isset($arr[$key]);
    }
    echo '^ | isset |  ', microtime(true) - $startTime, PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        array_key_exists($key, $arr);
    }
    echo '^ | array_key_exists | ', microtime(true) - $startTime, PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        isset($arr[$key]) || array_key_exists($key, $arr);
    }
    echo '^ | isset or array_key_exists | ', microtime(true) - $startTime, PHP_EOL;
}
PHP 5.6
-|函数|执行时间(s)
$arr['name']--
^isset0.64719796180725
^array_key_exists2.5713651180267
^isset or array_key_exists1.1359150409698
$arr['age']--
^isset0.53988218307495
^array_key_exists2.7240340709686
^isset or array_key_exists2.9613540172577
PHP 7.2.4
-|函数|执行时间(s)
$arr['name']--
^isset0.24308800697327
^array_key_exists0.3645191192627
^isset or array_key_exists0.28933310508728
$arr['age']--
^isset0.23279714584351
^array_key_exists0.33850502967834
^isset or array_key_exists0.54935812950134

2.4 性能-使用VLD查看opcode

/usr/local/Cellar/php/7.2.7/bin/php -d vld.active=1 -dvld.verbosity=3 vld.php
描述issetarray_key_exists
code$arr = ['name' => 'li']; isset($arr['name']);$arr = ['name' => 'li']; array_key_exists('name', $arr);
-dvld.active=1imageimage
-dvld.verbosity=3imageimage

3、源码

3.1 isset 源码分析

Zend/zend_language_scanner.l (Scanning阶段)

Scanning阶段,程序会扫描zend_language_scanner.l文件将代码文件转换成语言片段。

<ST_IN_SCRIPTING>"isset" {
    RETURN_TOKEN(T_ISSET);
}

可见 isset 生成对应的token为 T_ISSET

3.1.2 Zend/zend_language_parser.y (Parsing阶段)

当执行PHP源码,会先进行语法分析,isset的yacc如下:
接下来就到了Parsing阶段,这个阶段,程序将 T_ISSET 等Tokens转换成有意义的表达式,此时会做语法分析,Tokens的yacc保存在zend_language_parser.y文件中。isset的yacc如下(T_ISSET):

internal_functions_in_yacc:
        T_ISSET '(' isset_variables ')' { $$ = $3; }
    |    T_EMPTY '(' expr ')' { $$ = zend_ast_create(ZEND_AST_EMPTY, $3); }
    |    T_INCLUDE expr
            { $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE, $2); }
    |    T_INCLUDE_ONCE expr
            { $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE_ONCE, $2); }
    |    T_EVAL '(' expr ')'
            { $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_EVAL, $3); }
    |    T_REQUIRE expr
            { $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE, $2); }
    |    T_REQUIRE_ONCE expr
            { $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE_ONCE, $2); }
;

isset_variables:
        isset_variable { $$ = $1; }
    |    isset_variables ',' isset_variable
            { $$ = zend_ast_create(ZEND_AST_AND, $1, $3); }
;

isset_variable:
        expr { $$ = zend_ast_create(ZEND_AST_ISSET, $1); }
;

%%
/* Zend/zend_ast.c */
# zend_ast_export_ex
case ZEND_AST_EMPTY:
    FUNC_OP("empty");
case ZEND_AST_ISSET:
    FUNC_OP("isset");

最终执行了zend_ast_create(ZEND_AST_ISSET, $1);

我们知道, PHP7开始, 语法解析过程的产物保存于CG(AST),接着zend引擎会把AST进一步编译为 zend_op_array ,它是编译阶段最终的产物,也是执行阶段的输入

3.1.3 Zend/zend_compile.c(将表达式编译成opcodes)

将表达式编译成opcodes,可见isset对应的opcodes为ZEND_AST_ISSET。打开zend_compile.c文件

# void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */
    
case ZEND_AST_ISSET:
case ZEND_AST_EMPTY:
    zend_compile_isset_or_empty(result, ast);
    return;

最终执行了zend_compile_isset_or_empty函数,在源码目录中查找, 可以发现,此函数也在 zend_compile.c 文件中定义。

void zend_compile_isset_or_empty(znode *result, zend_ast *ast) /* {{{ */
{
    zend_ast *var_ast = ast->child[0];

    znode var_node;
    zend_op *opline = NULL;

    ZEND_ASSERT(ast->kind == ZEND_AST_ISSET || ast->kind == ZEND_AST_EMPTY);

    if (!zend_is_variable(var_ast) || zend_is_call(var_ast)) {
        if (ast->kind == ZEND_AST_EMPTY) {
            /* empty(expr) can be transformed to !expr */
            zend_ast *not_ast = zend_ast_create_ex(ZEND_AST_UNARY_OP, ZEND_BOOL_NOT, var_ast);
            zend_compile_expr(result, not_ast);
            return;
        } else {
            zend_error_noreturn(E_COMPILE_ERROR,
                "Cannot use isset() on the result of an expression "
                "(you can use \"null !== expression\" instead)");
        }
    }

    switch (var_ast->kind) {
        case ZEND_AST_VAR:
            if (is_this_fetch(var_ast)) {
                opline = zend_emit_op(result, ZEND_ISSET_ISEMPTY_THIS, NULL, NULL);
            } else if (zend_try_compile_cv(&var_node, var_ast) == SUCCESS) {
                opline = zend_emit_op(result, ZEND_ISSET_ISEMPTY_VAR, &var_node, NULL);
                opline->extended_value = ZEND_FETCH_LOCAL | ZEND_QUICK_SET;
            } else {
                opline = zend_compile_simple_var_no_cv(result, var_ast, BP_VAR_IS, 0);
                opline->opcode = ZEND_ISSET_ISEMPTY_VAR;
            }
            break;
        case ZEND_AST_DIM:
            opline = zend_compile_dim_common(result, var_ast, BP_VAR_IS);
            opline->opcode = ZEND_ISSET_ISEMPTY_DIM_OBJ;
            break;
        case ZEND_AST_PROP:
            opline = zend_compile_prop_common(result, var_ast, BP_VAR_IS);
            opline->opcode = ZEND_ISSET_ISEMPTY_PROP_OBJ;
            break;
        case ZEND_AST_STATIC_PROP:
            opline = zend_compile_static_prop_common(result, var_ast, BP_VAR_IS, 0);
            opline->opcode = ZEND_ISSET_ISEMPTY_STATIC_PROP;
            break;
        EMPTY_SWITCH_DEFAULT_CASE()
    }

    result->op_type = opline->result_type = IS_TMP_VAR;
    opline->extended_value |= ast->kind == ZEND_AST_ISSET ? ZEND_ISSET : ZEND_ISEMPTY;
}
/* }}} */

从这个函数最后一行可以看出,最终执行的还是ZEND_ISSET, 根据不同的用法会使用不同的opcode处理, 此处以ZEND_ISSET_ISEMPTY_DIM_OBJ为例。

3.1.4 Zend/zend_vm_execute.h (执行opcodes)

opcode 对应处理函数的命名规律:

ZEND_[opcode]_SPEC_(变量类型1)_(变量类型2)_HANDLER

变量类型1和变量类型2是可选的,如果同时存在,那就是左值和右值,归纳有下几类: VAR TMP CV UNUSED CONST 这样可以根据相关的执行场景来判定。

zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV_HANDLER,
             

我们看下 ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV_HANDLER 这个处理函数

if (opline->extended_value & ZEND_ISSET) {
    /* > IS_NULL means not IS_UNDEF and not IS_NULL */
    result = value != NULL && Z_TYPE_P(value) > IS_NULL &&
        (!Z_ISREF_P(value) || Z_TYPE_P(Z_REFVAL_P(value)) != IS_NULL);
} else /* if (opline->extended_value & ZEND_ISEMPTY) */ {
    result = (value == NULL || !i_zend_is_true(value));
}

上面的 if ... else 就是判断是isset,还是empty,然后做不同处理,Z_TYPE_P, i_zend_is_true 不同判断。

可见,isset的最终实现是通过 Z_TYPE_P 获取变量类型,然后再进行判断的。

函数的完整定义请查看Zend/zend_vm_execute.h,以下是 i_zend_is_trueZ_TYPE_P的定义:

3.2 array_key_exists 源码分析

3.2.1 ext/standard/array.c (数组扩展中实现)

array_key_exists是php内置函数,通过扩展方式实现的。打开php源码,ext/standard/目录下

// ➜  standard git:(master) ✗ grep -r 'PHP_FUNCTION(array_key_exists)' *

array.c: PHP_FUNCTION(array_key_exists)
php_array.h: PHP_FUNCTION(array_key_exists);

具体实现如下:

/* {{{ proto bool array_key_exists(mixed key, array search)
   Checks if the given key or index exists in the array */
PHP_FUNCTION(array_key_exists)
{
    zval *key;                    /* key to check for */
    HashTable *array;            /* array to check in */

#ifndef FAST_ZPP
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "zH", &key, &array) == FAILURE) {
        return;
    }
#else
    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_ZVAL(key)
        Z_PARAM_ARRAY_OR_OBJECT_HT(array)
    ZEND_PARSE_PARAMETERS_END();
#endif

    switch (Z_TYPE_P(key)) {
        case IS_STRING:
            if (zend_symtable_exists_ind(array, Z_STR_P(key))) {
                RETURN_TRUE;
            }
            RETURN_FALSE;
        case IS_LONG:
            if (zend_hash_index_exists(array, Z_LVAL_P(key))) {
                RETURN_TRUE;
            }
            RETURN_FALSE;
        case IS_NULL:
            if (zend_hash_exists_ind(array, ZSTR_EMPTY_ALLOC())) {
                RETURN_TRUE;
            }
            RETURN_FALSE;

        default:
            php_error_docref(NULL, E_WARNING, "The first argument should be either a string or an integer");
            RETURN_FALSE;
    }
}
/* }}} */

可以看到, 是通过 Z_TYPE_P 宏获取变量类型, 通过 zend_hash 相关函数判断 key 是否存在。以key为字符串为例,在Zend/zend_hash.h追踪具体实现:

3.2.2 Zend/zend_hash.h

ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key);

...

static zend_always_inline int zend_symtable_exists_ind(HashTable *ht, zend_string *key)
{
    zend_ulong idx;

    if (ZEND_HANDLE_NUMERIC(key, idx)) {
        return zend_hash_index_exists(ht, idx);
    } else {
        return zend_hash_exists_ind(ht, key);
    }
}

static zend_always_inline int zend_hash_exists_ind(const HashTable *ht, zend_string *key)
{
    zval *zv;

    zv = zend_hash_find(ht, key);
    return zv && (Z_TYPE_P(zv) != IS_INDIRECT ||
            Z_TYPE_P(Z_INDIRECT_P(zv)) != IS_UNDEF);
}

再次先通过函数ZEND_HANDLE_NUMERIC对key做判断,看这个字符串是不是数字类型的, 当key为数字时执行 zend_hash_index_exists, 实现如下:

3.2.3 Zend/zend_hash.c

3.2.3.1 zend_hash_index_exists()
/**
 * 这里有一个宏HASH_FLAG_PACKED,为真就代表当前数组的key都是系统生成的,也就是说是按从0到1,2,3等等按序排列的,所以判读键为key的是否存在,直接检查arData数组中第idx个元素是否有定义就行了,这里不涉及什么hash查找,冲突解决等一系列问题。
 *  
 * 但如果HASH_FLAG_PACKED为假,那么肯定就需要先计算idx的hash值,找到key为idx的数据应该在arData的第几位才行。这就要通过函数zend_hash_index_find_bucket了。
 */
ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zend_ulong h)
{
    Bucket *p;

    IS_CONSISTENT(ht);

    if (ht->u.flags & HASH_FLAG_PACKED) {
        if (h < ht->nNumUsed) {
            if (Z_TYPE(ht->arData[h].val) != IS_UNDEF) {
                return 1;
            }
        }
        return 0;
    }

    p = zend_hash_index_find_bucket(ht, h);
    return p ? 1 : 0;
}
3.2.3.2 zend_hash_find()

Zend/zend_hash.c中有zend_hash_find()的实现, code如下:

/*++-- zend_hash_find --++*/
/* Returns the hash table data if found and NULL if not. */
ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key)
{
    Bucket *p;

    IS_CONSISTENT(ht);

    p = zend_hash_find_bucket(ht, key);
    return p ? &p->val : NULL;
}
3.2.3.3 zend_hash_index_find_bucket()
static zend_always_inline Bucket *zend_hash_index_find_bucket(const HashTable *ht, zend_ulong h)
{
    uint32_t nIndex;
    uint32_t idx;
    Bucket *p, *arData;

    arData = ht->arData;
    nIndex = h | ht->nTableMask;
    idx = HT_HASH_EX(arData, nIndex);
    while (idx != HT_INVALID_IDX) {
        ZEND_ASSERT(idx < HT_IDX_TO_HASH(ht->nTableSize));
        p = HT_HASH_TO_BUCKET_EX(arData, idx);
        if (p->h == h && !p->key) {
            return p;
        }
        idx = Z_NEXT(p->val);
    }
    return NULL;
}
3.2.3.4 zend_hash_find_bucket()
static zend_always_inline Bucket *zend_hash_find_bucket(const HashTable *ht, zend_string *key)
{
    zend_ulong h;
    uint32_t nIndex;
    uint32_t idx;
    Bucket *p, *arData;

    h = zend_string_hash_val(key);
    arData = ht->arData;
    nIndex = h | ht->nTableMask;
    idx = HT_HASH_EX(arData, nIndex);
    while (EXPECTED(idx != HT_INVALID_IDX)) {
        p = HT_HASH_TO_BUCKET_EX(arData, idx);
        if (EXPECTED(p->key == key)) { /* check for the same interned string */
            return p;
        } else if (EXPECTED(p->h == h) &&
             EXPECTED(p->key) &&
             EXPECTED(ZSTR_LEN(p->key) == ZSTR_LEN(key)) &&
             EXPECTED(memcmp(ZSTR_VAL(p->key), ZSTR_VAL(key), ZSTR_LEN(key)) == 0)) {
            return p;
        }
        idx = Z_NEXT(p->val);
    }
    return NULL;
}

这里需要明白一点,数字的哈希值就等于他本身,所以才有不计算h的哈希值,就执行h | ht->nTableMask。

然后处理一下冲突,最后得出key为idx的数据是否存在于数组中。

如果idx确确实实是字符串,那么思路更简单一点,最后通过zen_hash_find_bucket来判断是否存在,与上面zend_hash_index_find_bucket不同的是,函数中要先计算字符串key的哈希值,然后再执行h | ht->nTableMask。

如下,

    zend_symtable_exists_ind -->ZEND_HANDLE_NUMERIC{ZEND_HANDLE_NUMERIC}
    ZEND_HANDLE_NUMERIC --> zend_hash_index_exists
    ZEND_HANDLE_NUMERIC --> zend_hash_exists_ind
    zend_hash_index_exists-->zend_hash_index_find_bucket
    zend_hash_exists_ind-->zend_hash_find
    zend_hash_find-->zend_hash_find_bucket

4、总结

  • isset效率高于array_key_exists, PHP7之后有30%左右的提升, php5.6有将近70%的提升。
  • isset是语法结构, array_key_exists是函数, 调用开销要小。
  • isset通过 Z_TYPE_P 获取变量类型,然后再进行判断实现的; array_key_exists则是通过hash查找来实现的。
  • 对于数组,isset的性能要高于array_key_exists 所以,如果数组比较大,我们应该用如下方法保证性能和准确性 isset or array_key_exists

5、扩展阅读

查看原文

赞 30 收藏 16 评论 5

em0t 关注了用户 · 2018-10-08

汤青松 @songboy

《PHP Web安全开发实战》 作者

关注 5938

em0t 赞了文章 · 2018-10-05

使用 javascript 替换 jQuery

使用 javascript 替换 jQuery

jQuery 曾风靡一个时代,大大降低了前端开发的门槛,丰富的插件也是前端开发者得心应手的武器库,但是,这个时代终于要落幕了。随着 JS 标准和浏览器的进步,jQuery 的很多精华被原生 JS 吸收,我们直接使用原生 API 就可以用类似手法来处理以前需要 jQuery 的问题。在新的 Web 项目中,如果不需要支持过于陈旧的浏览器版本,那么的确没有必要使用 jQuery。

下面就探讨如何用JavaScript(ES6)标准语法,取代jQuery的一些主要功能。

选取元素

选择器查询

常用的 class、id、属性 选择器都可以使用 document.querySelector 或 document.querySelectorAll 替代。

  • document.querySelector 返回第一个匹配的 Element
  • document.querySelectorAll 返回所有匹配的 Element 组成的 NodeList。

jQuery:

var $ele = $("selector");

Native:

let ele = document.querySelectorAll("selector");

选择器模式

选择器示例示例说明
.class.intro选择所有class="intro"的元素
#id#firstname选择所有id="firstname"的元素
**选择所有元素
elementp选择所有<p>元素
element,elementdiv,p选择所有<div>元素和<p>元素
element elementdiv p选择<div>元素内的所有<p>元素
element>elementdiv>p选择所有父级是<div>元素的 <p>元素
element+elementdiv+p选择所有紧接着<div>元素之后的<p>元素
[attribute=value]a[target=_blank]选择所有使用target="_blank"的<a>元素
[attribute^=value]a[src^="http"]选择每一个src属性的值以"http"开头的<a>元素
[attribute$=value]a[src$=".jpg"]选择每一个src属性的值以".jpg"结尾的<a>元素
:first-childul li:first-child选择<ul>元素下的首个<li>元素
:nth-child(n)ul li:nth-child(3)选择<ul>元素下的第三个<li>元素
:last-childul li:last-child选择<ul>元素下的最后一个<li>元素

DOM 树查询

jQueryNative方法说明
$ele.parent()ele.parentNode元素的直接父元素
$ele.children()ele.childNodes元素的所有直接子元素
$ele.find("a")ele.querySelectorAll("a")元素的后代元素
$ele.prev()ele.previousElementSibling元素的上一个同胞元素
$ele.next()ele.nextElementSibling元素的下一个同胞元素

DOM 操作

DOM本身就具有很丰富的操作方法,可以取代jQuery提供的操作方法。

内容和属性

jQueryNative方法说明
var text = $ele.text()let text = ele.innerText获取所选元素的文本内容
$ele.text("text")ele.innerText = "text"设置所选元素的文本内容
var html = $ele.html()let html = ele.innerHTML获取所选元素的HTML内容
$ele.html("<div>html</div>")ele.innerHTML = "<div>html</div>"设置所选元素的HTML内容
var input = $ele.val()let input = ele.value获取表单字段的值
$ele.val("input")ele.value = "input"设置表单字段的值
var href = $ele.attr("href")let href = ele.getAttribute("href")获取元素的属性值
$ele.attr("href", "/")ele.setAttribute("href", "/")设置元素的属性值

修改 DOM 树

jQueryNative方法说明
$parent.append($ele)parent.appendChild(ele)在被选元素的结尾插入内容
$parent.prepend($ele)parent.insertBefore(ele, parent.firstChild)在被选元素的开头插入内容
$ele.after(html)ele.insertAdjacentHTML("afterend", html)在被选元素之后插入内容
$ele.before(html)ele.insertAdjacentHTML("beforebegin", html)在被选元素之前插入内容
$ele.remove()ele.parentNode.removeChild(ele)删除被选元素及其子元素
$ele.empty()ele.innerHTML = null从被选元素中删除子元素
$ele.clone()ele.cloneNode(true)拷贝被选元素
$ele.replaceWith(html)ele.outerHTML = html指定HTML替换被选元素

CSS 样式

设置 Style

HTML DOM 允许 JavaScript 改变 HTML 元素的样式,Native API 提供了如下几种方式:

  • ele.setAttribute 直接修改 DOM style 属性改变样式
  • ele.style.cssText 通过 cssText 修改 Style 属性
  • ele.style.property 通过 style 对象读写行内 CSS 样式

jQuery:

var size = $ele.css("font-size"); // 返回第一个匹配元素的 CSS 属性值
$ele.css("font-size", "2rem"); // 为所有元素设置指定的 CSS 属性值

Native:

let size = getComputedStyle(ele)["font-size"]; // 获取当前元素计算后的 CSS 属性值
ele.style.setProperty("font-size", "2rem"); // 设置当前元素的某个内联样式
ele.style.removeProperty("font-size");  // 移除当前元素的某个内联样式

设置 Class

jQueryNative方法说明
$ele.hasClass(className)ele.classList.contains(className)检查元素是否包含指定的类名
$ele.addClass(className)ele.classList.add(className)向元素增加一个或多个类名
$ele.removeClass(className)ele.classList.remove(className)从元素中移除一个或多个类
$ele.toggleClass(className)ele.classList.toggle(className)对元素的一个或多个类进行切换

事件方法

绑定事件

jQuery:

$ele.on("click", function (evt) {
    console.log(evt.target);
});

Native:

ele.addEventListener("click", evt => {
    console.log(evt.target);
});

解除绑定

jQuery:

$ele.off("click");

Native:

ele.removeEventListener("click", func);

如果要移除事件,addEventListener 必须使用外部函数,绑定匿名函数的事件是无法移除的。

模拟触发

jQuery:

$ele.trigger("click");

Native:

let event = document.createEvent("MouseEvents");
event.initMouseEvent("click");
ele.dispatchEvent(event);

模拟事件:

  1. 首先通过 document.createEvent 方法创建 Event 对象。
  2. 然后利用 Event 对象的 init 方法对其进行初始化。
  3. 最后使用 dispatchEvent 方法触发 Event 对象。

详见:JavaScript 事件——“模拟事件”的注意要点

Ajax

jQuery

$.ajax({
    url: "http://apis.juhe.cn/ip/ip2addr",
    type: "GET",
    data: {
        "key": "80701ec21437ca36ca466af27bb8e8d3",
        "ip": "220.181.57.216"
    },
    dataType: "json",
    success: function (data) {
        console.log(data);
    }
});

XHR 封装

window.ajax = async function (params, callback) {
    let url = params.url;
    let method = params.method;
    let data = params.data;
    let body = new FormData();
    for (let key in data) {
        if (data.hasOwnProperty(key)) {
            body.append(key, data[key]);
        }
    }
    let xhr = new XMLHttpRequest();
    xhr.timeout = 3000;
    xhr.open(method, url, true);
    xhr.addEventListener("readystatechange", evt => {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                callback(xhr.response);
            } else {
                throw xhr.statusText;
            }
        }
    });
    xhr.send(body);
};

ajax({
        url: "http://apis.juhe.cn/ip/ip2addr",
        method: "GET",
        data: {
            "key": "80701ec21437ca36ca466af27bb8e8d3",
            "ip": "220.181.57.216"
        }
    },function (resp) {
        var json = JSON.parse(resp);
        console.log(json);
    }
)

Fetch API

XMLHttpRequest 并不是专为 Ajax 而设计的. 虽然各种框架对 XHR 的封装已经足够好用, 但更好用的 API 是 fetch 。

/* 构造请求对象 */
let request = new Request(
    "http://apis.juhe.cn/ip/ip2addr",
    {
        method: "GET",
        body: {
            "key": "80701ec21437ca36ca466af27bb8e8d3",
            "ip": "220.181.57.216"
        },
        headers: new Headers()
    }
);
/* 处理响应对象 */
fetch(request)
    .then(response => response.json())
    .then(function (data) {
        console.log(data);
    })
    .catch(function (error) {
        console.log(error);
    });

详见:fetch用法说明

工具

Array

jQueryNative方法说明
$.isArray(array)Array.isArray(array)判断参数是否为一个数组
$.inArray(item, array)array.includes(item)判断值是否在指定数组中
$.makeArray(objlist)Array.from(objlist)将类数组对象转换为数组
$.merge(array1, array2)array1.concat(array2)合并两个数组(有区别)
$.each(array, function (i, item) {}array.forEach((item, i) => {})遍历指定的对象和数组

合并数组时,merge 会改变原数组的内容,而 concat 不会修改原数组,只会返回合并后的数组

Method

jQueryNative方法说明
$.now()Date.now()返回当前时间戳
$.trim(context)context.trim()移除字符串头尾空白
$.type(parameter)typeof parameter检测参数的内部类型
$.parseJSON(jsonstr)JSON.parse(jsonstr)将JSON转换为JS对象
$ele.data("key", "value")ele.dataset.key = "value"在指定的元素上存储数据
$.map(array, function (item, i) {})array.map((item, i) => {})将数组转化为处理后的新数组
查看原文

赞 132 收藏 110 评论 5

em0t 赞了文章 · 2018-10-05

js骚操作骂人不带脏

前言

很多小伙伴们觉得javaScript很简单,下面的这行 javaScript代码可能会让你怀疑人生。


(!(~+[])+{})[--[~+""][+[]]*[~+[]] + ~~!+[]]+({}+[])[[~!+[]]*~+[]]

QQ截图20181001103352.png

小伙伴们学会了,以后遇到一些讲不通道理,让你不开森的人就把这段代码发给他。

那么这段代码为什么会输出sb呢?

其实这段代码考的是js的类型转化的一些基本原理

首先要运用到的第一个知识就是js运算符的优先级,因为这么长一段运算看的人眼花,我们必须得先根据优先级分成n小段,然后再各个击破

1, js运算符的优先级

优先级的排列如下表:

  优先级从高到低:838ba61ea8d3fd1fc89cb337364e251f95ca5f21.jpg

根据此规则,我们把这一串运算分为以下16个子表达式:
2e2eb9389b504fc28eefa108e3dde71190ef6d0e.jpg

运算符用红色标出,有一点可能大家会意识不到,其实中括号[]也是一个运算符,用来通过索引访问数组项,另外也可以访问字符串的子字符,有点类似charAt方法,如:'abcd'[1] // 返回'b'。而且中括号的优先级还是最高的哦。

接下来需要运用的就是javascript的类型转化知识了,我们先说说什么情况下需要进行类型转化。当操作符两边的操作数类型不一致或者不是基本类型(也叫原始类型)时,需要进行类型转化。

让我们快速的复习一下.在JavaScript中,一共有两种类型的值:原始值(primitives)和对象值(objects).

  • 原始值有:undefined, null, 布尔值(booleans), 数字(numbers),还有字符串(strings).
  • 其他的所有值都是对象类型的值,包括数组(arrays)和函数(functions).

2.类型转化

(1)先按运算符来分一下类:

  • 减号-,乘号*,肯定是进行数学运算,所以操作数需转化为number类型。
  • 加号+,可能是字符串拼接,也可能是数学运算,所以可能会转化为number或string
  • 一元运算,如+[],只有一个操作数的,转化为number类型

(2)下面来看一下转化规则。

(2).1 对于非原始类型的,通过ToPrimitive() 将值转换成原始类型:

  ToPrimitive(input, PreferredType?)

  可选参数PreferredType是Number或者是String。返回值为任何原始值.如果PreferredType是Number,执行顺序如下:

如果input为primitive,返回

  否则,input为Object。调用 obj.valueOf()。如果结果是primitive,返回。

  否则,调用obj.toString(). 如果结果是primitive,返回

  否则,抛出TypeError

  如果 PreferredType是String,步骤2跟3互换,如果PreferredType没有,Date实例被设置成String,其他都是Number

(2).2 通过ToNumber()将值转换为数字

通过ToNumber()把值转换成Number,直接看ECMA 9.3的表格

参数结果
undefinedNaN
null+0
布尔值true被转换为1,false转换为+0
数字无需转换
字符串由字符串解析为数字.例如,"324"被转换为324

如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, Number)将该对象转换为原始值,然后在调用ToNumber()将这个原始值转换为数字.

(2).3 通过ToString()将值转换为字符串

通过ToString()把值转化成字符串, 直接看ECMA 9.8的表格

参数结果
undefined"undefined"
null"null"
布尔值"true" 或者 "false"
数字数字作为字符串,比如. "1.765"
字符串无需转换

如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, String)将该对象转换为原始值,然后再调用ToString()将这个原始值转换为字符串.

规则就这么多,接下来实践一下,根据我们上面划分出的子表达式,一步一步将这个神奇的代码给执行出来。开工~

先看最简单的子表达式16:+[]

  只有一个操作数[],肯定是转化为number了,根据上面的规则2,[]是个数组,object类型,即对象。所以得先调用toPrimitive转化为原始类型,并且PreferredType为number,这个参数表示更“倾向于”转化的类型,这里肯定是number了。然后首先调用数组的valueOf方法,数组调用valueOf会返回自身,如下:
  12.jpg
  
  这个时候,我们得到一个空串“”,还没有结束,看上面的规则2描述,继续调用toNumber,转化为number类型,如下:
  
23.jpg

大功告成!子表达式16转化完毕,+[],最终得到0。

  来看子表达式15:[~+""]

  空串""前面有两个一元操作符,但是操作数还是只有一个,所以,最终要转化为的类型是number。看规则2吧,空串调用toNumber得到0。接下来是~,这是个什么东东呢?它是位运算符,作用可以记为把数字取负然后减一,所以~0就是-1 。

  别忘了,这个子表达式外头还包着中括号,所以最终的值为[-1],即一个数组,里面只有一个元素-1.

  接下来看子表达式13就简单了,把15、16求出来的填进去,就变成了这样:---1,取数组的第0个元素,然后自减,结果为-2,是不so easy!

  继续往上走,子表达式14: [~+[]]

  其实把15、和16的原理用上就非常明显了,答案[-1]

  继续来求子表达式9,此刻它已变成:-2[-1],有稍许不一样,不过没关系,我们还是按照规则来,运算符是乘号,当然是做数学运算,那后面的[-1]就得转化为number,与16的求法类似,过程如下:

  ①调用toPrimitive,发现是object类型

  ②调用valueOf,返回自身[-1]

  ③因为不是原始类型,继续调用toString,返回"-1"

  ④"-1"是原始类型了,然后调用toNumber,返回-1

  ⑤与-2相乘,返回2

  子表达式10:~~!+[],不多说了,答案1. 就是从右往左依次一元计算。

  有了9和10,我们来到了子表达式4,此刻它已经长这样了:2+1, 好,我不多说了。

  继续看表达式7:!(~+[]),~+[]=-1,这个根据上面已经知道了,那!-1是什么呢?这里要说一下这个感叹号,它是逻辑取非的意思,会把表达式转化为布尔类型,转化规则和js的Truthy和Falsy原则是一样的,后面跟数字的,除0以外都为false,后面跟字符串的,除空串以外都为false。这里的!-1当然就是false了。

  接下来这个表达式3:false+{}有点关键。一个布尔加一个对象,那这个{}应该先转化为原始类型,流程如下:

  ①调用toPrimitive,发现是object类型

  ②调用valueOf,返回自身{},
  ③不是原始类型,调用toString,返回"[object Object]"

  ④false与"[object Object]"相加,false先转化为字符串"false"

  ⑤相加得结果"false[object Object]"

  知道了表达式3和4,我们就可以来看表达式1了,此时它是这样的:"false[object Object]"[3],因为这个[]可以取字符串的子字符,像charAt一样,所以得到了结果"s"

  经过上面艰难的流程,我们拿到了字符"s",也就是那张图的左半边,剩下的那个"b",相同的原理可以搞出来,我这里就不一一演示了,留给你练练吧~

  回顾一下这个过程其实也不复杂,只是有一些需要重复劳动的,只要你掌握了运算的优先级,能把大串分解成一个个小串,然后运用类型转化的知识挨个处理就搞定了。怎么样,看到这里你还觉得神奇吗?

同样的,中文字符也是由这样组成的,跟英文同样的道理。

参考

https://www.cnblogs.com/ziyun...
https://zhidao.baidu.com/ques...

查看原文

赞 41 收藏 29 评论 7

认证与成就

  • 获得 46 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-04-12
个人主页被 1.6k 人浏览