首页 文章 精选 留言 我的

精选列表

搜索[磐石100],共10009篇文章
优秀的个人博客,低调大师

使用 Docker 简化 TeamCity 安装 【已翻译100%】

JetBrains出品的TeamCity是一款强大易用的持续集成(continuous integration)系统。它是一款商业产品,但对于小型项目和FOSS应用程序(免费和开源软件)有一项特殊的零费用许可证。尽管TeamCity的安装已经相对容易了,但通过使用Docker,安装过程会更简便。 像许多其它的最新式的持续集成系统一样,TeamCity采用了构建服务器和构建代理的概念。服务器负责管理和构建配置。实际的构建过程(编译、打包、部署等等)是由一个或多个代理执行的。通过个种方式,很容易实现代理的自动设置,整个设置过程中只需要很少的手动配置。 TeamCity服务器只需要Java。安装过程很简单。通过Docker,安装会更简单。为此,我准备了一个专门的容器 ariya/centos6-teamcity-server。此容器的基本系统是ariya/centos6-oracle-jre7,一个运行了Oracle官方Java 7的CentOS 6.5系统(更准确地说,到发稿为止是JRE 1.7.0_65-b17)。 假定你有一个已经安装了Docker的系统(VPS例如:Linode或DigitalOcean,Amazon EC2实例,一个虚拟机或一部真正的计算机),安装TeamCity服务器就简单到只需要运行下列命令。注意,如果你在使用OS X,如果你仅想试验本安装请使用boot2docker(参看我之前发表的博客 Docker on OS X 了解更多细节)。 docker run -dt -name teamcity_server -p 8111:8111 ariya/centos6-teamcity-server 几分钟后,打开机器地址的8111端口,开始TeamCity服务器的Web配置(更多细节请阅读官方TeamCity文档),如下图所示。如果你的系统使用了iptalbes,确保8111端口允许连接。注意,TeamCity数据将存储在特殊的位置/data/teamcity。这是一个标准的Docker卷,应当允许简单挂载、备份和未来升级。 服务器配置完成后,就应为此服务器指定构建代理(不然,什么也构建不了)。通过运行容器ariya/centos-teamcity-agent,我们再次用Docker产生一个构建代理。为了让代理工作,我们需要为它指定服务器。运行下列命令: docker run -e TEAMCITY_SERVER=http://buildserver:8111 \ -dt -p 9090:9090 ariya/centos6-teamcity-agent 如果你在运行着服务器容器的同一台主机上运行它,你需要将它们连接(link)起来。 docker run -e TEAMCITY_SERVER=http://teamcity_server:8111 \ --link teamcity_server:teamcity_server -dt ariya/centos6-teamcity-agent 环境变量中的TEAMCITY_SERVER是必须的,需要指向你上一步启动的TeamCity服务器的本地实例。一旦你运行了这个容器,它会连接到所指定的服务器,下载代理ZIP文件并安装。第一次连接服务器后,构建代理通常需要花几分钟时间更新。如果一切工作正常,在你的TeamCity服务器Web页面的Agent标签项中,你将看到一个新的代理。授权代理,现在它准备接受任何构建工作了! 如果启动代理出现问题,(docker ps)没有显示运行的容器,重新运行它,但这次使用 -it选项(interactive terminal交互终端)代替 -dt。这将转存一些额外的调试信息,有助于解决问题。 注意,此代理容器也是基于带Java 7的CentOS 6。通常这还不够,你可能需要其它的依赖项(不同的SDK,编译器,库等等)。理想情况下,那些依赖项能自动解决,或基于不同系统容器,或通过正确的自动设置。更多细节,参考我之前发表的博客 Build Agent: Template vs Provisioning。 还有借口不用持续集成?我可不这么想!

优秀的个人博客,低调大师

Docker 网络配置 【已翻译100%】(1/3)

摘要 当docker启动时,它会在宿主机器上创建一个名为docker0的虚拟网络接口。它会从RFC 1918定义的私有地址中随机选择一个主机不用的地址和子网掩码,并将它分配给docker0。例如当我启动docker几分钟后它选择了172.17.42.1/16-一个16位的子网掩码为主机和它的容器提供了65,534个ip地址。 注意: 本文讨论了Docker的高级网络配置和选项。通常你不会用到这些。如果你想查看一个较为简单的Docker网络介绍和容器概念介绍来着手,请参见Docker用户指南. 但docker0并不是正常的网络接口。它只是一个在绑定到这上面的其他网卡间自动转发数据包的虚拟以太网桥。它可以使容器与主机相互通信。每次Docker创建一个容器,它就会创建一对对等接口(peer interface),类似于一个管子的两端-在这边可以收到另一边发送的数据包。Docker会将对等接口中的一个做为eth0接口连接到容器上,并使用类似于vethAQI2QT这样的惟一名称来持有另一个,该名称取决于主机的命名空间。通过将所有veth*接口绑定到docker0桥接网卡上,Docker在主机和所有Docker容器间创建一个共享的虚拟子网。 本文其他部分将会讲解使用Docker选项的所有方式,并且-在高级模式下-使用纯linux网线配置命令来 调整,补充,或完全替代Docker的默认网络配置。 Docker选项快速指南 这里有一份关于Docker网络配置的命令行选项列表,省去您查找相关资料的麻烦。一些网络配置的命令行选项只能在服务器启动时提供给Docker服务器。并且一旦启动起来就无法改变。 一些网络配置命令选项只能在启动时提供给Docker服务器,并且在运行中不能改变: b BRIDGE或--bridge=BRIDGE— see 建立自己的网桥 -bip=CIDR— see 定制docker0 H SOCKET...或--host=SOCKET...— 它看起来像是在设置容器的网络,但实际却恰恰相反:它告诉Docker服务器要接收命令的通道,例如“run container"和"stop container"。 -icc=true|false— see 容器间通信 -ip=IP_ADDRESS— see 绑定容器端口 -ip-forward=true|false— see 容器间通信 -iptables=true|false— see 容器间通信 -mtu=BYTES— see 定制docker0 有两个网络配置选项可以在启动时或调用docker run时设置。当在启动时设置它会成为docker run的默认值: -dns=IP_ADDRESS...— see 配置DNS -dns-search=DOMAIN...— see 配置DNS 最后,一些网络配置选项只能在调用docker run时指出,因为它们要为每个容器做特定的配置: h HOSTNAME或--hostname=HOSTNAME— see 配置DNS 和 Docker与容器连接原理 -link=CONTAINER_NAME:ALIAS— see 配置DNS and 容器间通信 -net=bridge|none|container:NAME_or_ID|host— see Docker与容器连接原理 p SPECor--publish=SPEC— see 绑定容器端口 P或--publish-all=true|false— see 绑定容器端口 接下来的部分会对以上话题从易到难做出逐一解答。 配置DNS 怎样为Docker提供的每一个容器进行主机名和DNS配置,而不必建立自定义镜像并将主机名写到里面?它的诀窍是覆盖三个至关重要的在/etc下的容器内的虚拟文件,那几个文件可以写入新的信息。你可以在容器内部运行mount看到这个: $$ mount.../dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 .../dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ...tmpfs on /etc/resolv.conf type tmpfs ...... 这样的配置允许Docker去做聪明的事情,类似于当主机接收到新的DHCP配置之后,保持resolv.conf的数据到所有的容器中。Docker怎样维护在容器内的这些文件从Docker的一个版本到下一个版本的具体细节,你应该抛开这些单独的文件本身并且使用下面的Docker选项代替。 有四种不同的选项会影响容器守护进程的服务名称。 -h HOSTNAME 或者 --hostname=HOSTNAME --设置容器的主机名,仅本机可见。这种方式是写到/etc/hostname ,以及/etc/hosts 文件中,作为容器主机IP的别名,并且将显示在容器的bash中。不过这种方式设置的主机名将不容易被容器之外可见。这将不会出现在 docker ps 或者 其他的容器的/etc/hosts 文件中。 --link=CONTAINER_NAME:ALIAS --使用这个选项去run一个容器将在此容器的/etc/hosts文件中增加一个主机名ALIAS,这个主机名是名为CONTAINER_NAME 的容器的IP地址的别名。这使得新容器的内部进程可以访问主机名为ALIAS的容器而不用知道它的IP。--link= 关于这个选项的详细讨论请看: Communication between containers. --dns=IP_ADDRESS --设置DNS服务器的IP地址,写入到容器的/etc/resolv.conf文件中。当容器中的进程尝试访问不在/etc/hosts文件中的主机A时,容器将以53端口连接到IP_ADDRESS这个DNS服务器去搜寻主机A的IP地址。 --dns-search=DOMAIN --设置DNS服务器的搜索域,以防容器尝试访问不完整的主机名时从中检索相应的IP。这是写入到容器的/etc/resolv.conf文件中的。当容器尝试访问主机 host,而DNS搜索域被设置为 example.com ,那么DNS将不仅去查寻host主机的IP,还去查询host.example.com的IP。 在docker中,如果启动容器时缺少以上最后两种选项设置时,将使得容器的/etc/resolv.conf文件看起来和宿主主机的/etc/resolv.conf文件一致。这些选项将修改默认的设置。 容器间通信 在操作系统层面上,决定两个容器间的通信能否得到控制,有以下三个因素。 网络拓扑逻辑是否连接上了容器的网络接口。默认情况下Docker将把所有容器绑定到一个 singledocker0bridge,并为两个容器间的包传输提供路径。参见本文档后续部分---其他可能的拓扑逻辑 主机是否要发送IP包?这由ip_forward系统参数控制。如果这个参数设为1,那么数据包只能在容器间传输。通常情况下,让Docker服务器使用它的默认设置 --ip-forward=true , Docker在启动的时候会把ip_forwardsh. 要检查设置或手动设置参数,可以这样做: # Usually not necessary: turning on forwarding,# on the host where your Docker server is running$ cat /proc/sys/net/ipv4/ip_forward0$ sudo echo 1 > /proc/sys/net/ipv4/ip_forward $ cat /proc/sys/net/ipv4/ip_forward1 iptables是否允许特殊连接?如果你把设置 --iptables=false,当守护进程启动时,Docker不会改变你的系统iptables规则。另外,如果你保留默认设置 --icc=true,Docker服务器或向FORWARD链添加一个带有全局ACCEPT策略的默认规则。如果不保留默认设置,系统会把策略设为DROP. 几乎所有人使用docker都希望ip_forward 是打开的,至少使容器间的通讯成为可能。但是否同意 --icc=true 或者更改为 --icc=false 使得iptables 可以保护容器以及宿主主机不被任意地端口扫描、避免被已经被渗透的容器所访问,这是一个策略问题。(在ubuntu,是编辑/etc/default/docker文件中的DOCKER_OPTS参数,然后重启docker服务) 如果你选择最安全的设置 --icc=false ,那么当你想让它们彼此提供服务的时候如何让它们相互通讯? 答案是:使用前文提到的 --link=CONTAINER_NAME:ALIAS 选项。如果docker守护进程正在以 --icc=false 和 --iptables=true 参数运行,当以选项 --link= 执行 docker run 命令时,docker服务将插入一部分 iptables ACCEPT 规则使得新容器可以连接其他容器所暴露出来的端口(此端口指前文在 Dockerfile 中提到的EXPOSE这一行)。更多详细文档介绍请看:linking Docker containers。 注意: --link 选项中的 CONTAINER_NAME 的值必须是 docker自动分配的容器名称,比如 stupefied_pare, 或者是在执行docker run 的时候用 --name= 指定的容器名称. 这不能使一个docker无法识别的主机名。 你可以在你的Docker主机上运行iptables命令,来观察FORWARD链是否有默认的ACCEPT或DROP策略 # When --icc=false, you should see a DROP rule:$ sudo iptables -L -n...Chain FORWARD (policy ACCEPT)target prot opt source destination DROP all -- 0.0.0.0/0 0.0.0.0/0...# When a --link= has been created under --icc=false,# you should see port-specific ACCEPT rules overriding# the subsequent DROP policy for all other packets:$ sudo iptables -L -n...Chain FORWARD (policy ACCEPT)target prot opt source destination ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80DROP all -- 0.0.0.0/0 0.0.0.0/0 注意: Docker的iptables规则完全显示了容器相互间的原始IP地址,所以一个容器到另一个容器的连接,需要显示地显示出第一个容器的原始IP地址。 为主机绑定容器端口默认情况下,Docker容器可以连接到外部区域,但外部区域不能连接到容器。在Docker启动时,由于它在主机上创建了一个iptables伪装规则,使得每一个输出连接看起来都是由主机IP地址建立起来的。 # You can see that the Docker server creates a# masquerade rule that let containers connect# to IP addresses in the outside world:$ sudo iptables -t nat -L -n...Chain POSTROUTING (policy ACCEPT)target prot opt source destination MASQUERADE all -- 172.17.0.0/16 !172.17.0.0/16... 当调用docker run的时候,如果你想让容器接受输入连接,你需要提供特殊选项。这些选项的详细说明在 Docker User Guide. 有两种方法可以实现。

优秀的个人博客,低调大师

Docker 网络配置 【已翻译100%】(2/3)

首先,你可以提供 -P 或者 --publish-all=true|false 选项参数来执行 docker run 命令,这将会识别所有在dockerfile中暴露的端口,并且随机映射到 49000-49900 之间的主机端口。这看起来是一个很大的不便,当你要启动一个新的容器时你需要知道那个主机端口已经被映射。 更方便的操作是使用 -p SPEC 或者 --publish=SPEC 选项,这两个选项让你明确的指定docker容器的端口映射到任意的主机端口中,不局限于49000-49900. 无论如何,你应该通过审查你的NAT表,去看看docker在你的网络占做了什么。 # What your NAT rules might look like when Docker # is finished setting up a -P forward: $ iptables -t nat -L -n ...Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80 # What your NAT rules might look like when Docker # is finished setting up a -p 80:80 forward: Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 可以看到,docker暴露了这些容器的端口到通配IP地址:0.0.0.0 ,这个通配IP地址可以匹配宿主主机上任意一个可以进入的端口。如果你希望更多的限制,并且只允许容器服务通过特殊的宿主主机的外部网络接口来相互联系,那么你有两种选择。当你执行 docker run 命令时,你可以使用 -p IP:host_port:container_port 或者 -p IP::port 来明确地绑定外部接口。 或者如果你希望dokcer永远转发到一个特殊的IP地址上,你可以编辑你的docker系统设置文件(ubuntu系统的设置方法为:编辑 /etc/default/docker文件,改写DOCKER_OPTS参数),增加选项 --ip=IP_ADDRESS 。修改完之后记得重启你的docker服务。 如果你希望更详细的指导,请参考: Docker User Guide . 定制 docker0 默认地,docker服务会在linux内核新建一个网络桥接docker0,使得物理主机和其他虚拟网络接口之间可以传递发送数据包,因此,这表现如一个独立的网络。 docker0有一个IP地址和子网掩码,使得物理主机可以从容器的桥接网络接收和发送数据包。并且给这个桥接网络一个MTU(最大传输单元)或者说网络接口允许的最大包长度-例如1,500 bytes 或者从docker的宿主主机上的网络接口拷贝的数值。在服务启动的时候两者都是可配置的: --bip=CIDR— 为docker0桥接网络提供一个特殊的IP地址和一个子网掩码, 使用标准的 CIDR 记法例如192.168.1.5/24. --mtu=BYTES— 从写docker0的最大数据包长度。 在ubuntu系统上,你可以增加以上的配置到 /etc/default/docker 文件中的DOCKER_OPTS参数中,然后重启docker服务。 当你有一个或多个正常运行的容器时,你可以通过在主机上运行brct1命令,观察interfaces列的输出,来确定Docker已经将这些容器正确地连接到docker0网桥。下面是一个连接了两个不同容器的主机: # Display bridge info$ sudo brctl show bridge name bridge id STP enabled interfaces docker0 8000.3a1d7362b4ee no veth65f9 vethdda6 如果你的Docker主机还没安装brct1命令,那么你可以在Ubuntu上运行sudo apt-get install bridge-utils来安装它。 最后,每次新建一个容器的时候都会用到docker0 桥接网络。每次在执行docker run命令新建一个容器的时候,docker从可利用的桥接网络中随机选择一个未被使用的IP地址,以及使用桥接网络的子网掩码,用来配置容器 eth0网络接口。docker宿主主机的IP地址被docker容器作为默认的网关。 # The network, as seen from a container$ sudo docker run -i -t --rm base /bin/bash $$ ip addr show eth024: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 32:6f:e0:35:57:91 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::306f:e0ff:fe35:5791/64 scope link valid_lft forever preferred_lft forever $$ ip routedefault via 172.17.42.1 dev eth0172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.3$$ exit 记住docker的宿主主机无法转发docker容器的数据包到因特网上,除非它的ip_forward 系统设置为1,详情请看: Communication between containers 。 建立你自己的桥接网络如果你希望建立完整的自己的桥接网络,你可以在启动docker之前用 -b BRIDGE 或者 --bridge=BRIDGE选项参数高数docker使用你自己的桥接网络。如果你已经用docker0启动docker了,你需要停止docker服务然后移除docker0. # Stopping Docker and removing docker0$ sudo service docker stop $ sudo ip link set dev docker0 down $ sudo brctl delbr docker0 然后,在启动docker服务之前,新建你自己的桥接网络,写上你想要的配置。接下来我们新建一个简单的桥接网络,刚好用这些选项来定做docker0 ,这刚好足够说明这个技术。 # Create our own bridge$ sudo brctl addbr bridge0 $ sudo ip addr add 192.168.5.1/24 dev bridge0 $ sudo ip link set dev bridge0 up# Confirming that our bridge is up and running$ ip addr show bridge04: bridge0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state UP group default link/ether 66:38:d0:0d:76:18 brd ff:ff:ff:ff:ff:ff inet 192.168.5.1/24 scope global bridge0 valid_lft forever preferred_lft forever# Tell Docker about it and restart (on Ubuntu)$ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker $ sudo service docker start 运行结构应该是docker服务成功启动,已经准备好绑定容器到桥接网络上。当核实好桥接网络的配置之后,尝试着新建一个容器,你将看到容器的IP地址是在你的新的桥接网络范围内的,这是docker自动检测的。 正如前文所述,可以用 brctl show 命令查看,新增或者移除网络接口,可以在docker容器中执行 ip addr 和 ip route 命令查看IP地址是否是从网桥IP段分配的,以及docker的宿主主机的IP是否被作为默认网关。**Docker 如何使容器连接到网络** docker是正在发展中的,并会持续提升网络配置的逻辑。当前命令行是很难满足docker新建容器时所需要的网络配置。 让我们回顾一些基础知识。 通讯的时候使用网际协议(IP),一个机器需要访问至少一个网络接口用来发送和接收包,路由表定义了通过接口可达IP地址范围。网络接口不一定非是物理设备。实际上,在每一个Linux机器(和每个Docker容器内部)的lo回环接口都是有效的而且完全是虚拟的——Linux内核简单地拷贝回环(数据)包,直接从发送者的内存放入接收者的内存。 Docker使用特殊的虚拟接口让容器在主机间通讯——成对的虚拟接口被叫做“peers”,它被链接到主机内核的内部,因此(数据)包能在他们之间传输。他们简单创建,待会儿我们将会看到。

优秀的个人博客,低调大师

如何进入 Docker 容器 【已翻译100%】

在前几篇文章[1,2,3]里,Lukas Pustina简单地介绍了使用Docker进行系统级虚拟化。在这篇文章里,我将讨论四种连接Docker容器并与其进行交互的方法。例子中所有的代码都可以在GitHub中找到,你可以亲自对它们进行测试。 nsenter 从util-linux版本2.23开始,nsenter工具就包含在其中。它用来访问另一个进程的名字空间。nsenter要正常工作需要有root权限。很不幸,Ubuntu 14.4仍然使用的是util-linux版本2.20。安装最新版本的util-linux(2.24)版,请按照以下步骤: cd /tmp curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-cd util-linux-2.24./configure --without-ncursesmake nsentercp nsenter /usr/local/bin 为了连接到容器,你还需要找到容器的第一个进程的PID。 docker inspect --format "{{ .State.Pid }}" <container-id> 通过这个PID,你就可以连接到这个容器: nsenter --target $PID --mount --uts --ipc --net --pid nsinit 从0.9版本开始,Docker自身就具有一个管理容器的库,名字为 libcontainer。libcontainer中的nsinit工具允许用户直接访问linux名字空间和cgroup内核。在安装nsinit之前,你首先需要安装Go运行时环境: apt-get install git golang-go mkdir -p $HOME/go-dev/binmkdir -p $HOME/go-dev/src echo "export GOPATH=\$HOME/go-dev" >> ~/.profileecho "PATH=\$PATH:\$GOPATH/bin" >> ~/.profile source ~/.profile 接下来才安装nsinit: mkdir -p $GOPATH/src/github.com/dotcloudcd $GOPATH/src/github.com/dotcloud git clone https://github.com/dotcloud/docker.gitcd $GOPATH/src/github.com/dotcloud/docker /usr/bin/go get -v github.com/dotcloud/docker/vendor/src/github.com/docker/libcontainer/nsinit nsinit读取的是位于/var/lib/docer/execdriver/native/容器目录下的配置数据。要运行nsinit,你需要切换到容器目录下。由于/var/lib/docker目录对于root用户是只读权限,因此你还需要root权限。通过docker的ps命令,你可以确定容器ID。一旦你进入/var/lib/docker目录,你就可以连接容器了: nsinit exec /bin/bash lxc(-attach) 直到Docker 0.8.1版本为止,LXC一直是管理容器的基本工具,Docker一直支持这个工具。但是从0.9.0版本开始,Docker默认使用libcontainer管理容器,不再依赖LXC了。因此默认情况下,你不能使用lxc-attach了。 如果你仍然希望使用lxc-attach,那么你需要使用-e lxc选项来重新启动Docker服务进程。使用这个选项,Docker的内部将再次使用LXC管理容器了。完成这个任务最简单的做法就是创建/etc/default/docker文件(如果这个文件仍然不存在),并添加以下内容: DOCKER_OPTS=" -e lxc" 现在你可以重新启动Docker服务了。要连接容器,你需要知道完整的容器ID: docker ps --no-trunc 接下来,你就可以连接这个容器了。要完成下面工作,你还需要root权限: lxc-attach -n <container-id> -- /bin/bash sshd 上面所有三种方法都要求具有主机系统的root权限。为了不采用root权限,通过ssh访问容器将是一个很好的选择。 要做到这一点,你需要构建一个支持SSH服务的基础映像。此时,我们可能遇到这样的问题:我们是不是用Docker CMD或者ENTRYPOINT运行一条命令就可以了?如果此时有sshd进程运行,那么我们就不要再运行其他进程了。接下来的工作是创建一个脚本或者使用像supervisord这样的进程管理工具来启动其它所有需要启动的进程。有关如何使用supervisord的 优秀的文档可以在Docker的web站点上找到。一旦你启动了具有sshd进程的容器,你就可以像以往一样通过ssh客户端了连接这个容器了。 结论 sshd方法可能是最简单的连接容器的方法,而且大多数用户习惯通过ssh连接虚拟机。另外,连接容器时你也不需要一定使用root权限。不过,对于是否一个容器是否应当管理不止一个进程仍然存在许多争议。这种方法最终使得每个容器了多了一个sshd进程,这从根本上来说不是进程虚拟化的所提倡的。 另外三种方法都需要root权限。到0.8.1版本为止,Docker都是使用LXC来管理容器的。正是由于这个原因,使用lxc-attach连接容器就非常容易。不过从版本0.9.0开始Docker服务就必须使用 -e lxc选项启动才能在内部支持LXC管理容器。不过,由于设置了这个选项,Docker将再次依赖LXC,而LXC可能随着发布或者安装的不同可能被剔除。 nsenter和nsinit总的来说是相同的。这两个工具的主要区别是nsinit在本身的容器了建立了一个新的进程,而nsenter只是访问了名字空间。Jerome Petazzoni在Docker博客文章里对这一点说的很透彻。

优秀的个人博客,低调大师

Swift 如何实现手势识别 【已翻译100%】

在这次IOS应用开发教程中,我们打算实现手势识别。正如你所知道的,IOS支持大量的手势操作,它们能提供了很好的应用控制和出色用户体验。 让我们开始吧! 首先需要在Xcode中创建一个新的Single View Application: 然后点击Next,弹出的窗口要求你填写项目设置。在第一栏 (“Product name”) 中填入项目名称后,点击Next. 确保语言选择的是 “Swift”. 设计界面点击 “Main.storyboard” 文件,拖出6个 UIViews放到视图中.把视图排列成如图所示的样子.当你排列UIViews时,在每个view下面添加一个UILabel并依图设定文本值。 我们开始写代码吧. 是时候编辑实现文件了 (在我们的案例 “ViewController.swift” ). 为了声明一些我们将会用到的变量,要在 “class ViewController: UIViewController “块中添加如下代码. class ViewController: UIViewController { @IBOutlet var tapView: UIView @IBOutlet var swipeView: UIView @IBOutlet var longPressView: UIView @IBOutlet var pinchView: UIView @IBOutlet var rotateView: UIView @IBOutlet var panView: UIView var lastRotation = CGFloat() let tapRec = UITapGestureRecognizer() let pinchRec = UIPinchGestureRecognizer() let swipeRec = UISwipeGestureRecognizer() let longPressRec = UILongPressGestureRecognizer() let rotateRec = UIRotationGestureRecognizer() let panRec = UIPanGestureRecognizer() } 在第2 – 7行,我们声明了在之前界面里排列过的 UIViews. 在第8行,我们声明了实现旋转手势要用到的变量(lastRotation). 在第 9 – 14行,我们为每个view声明了一个手势识别对象. 注意: 在 Swift中,我们用let关键字声明常量,这意味着它的值在程序运行时不可改变。关键字var则声明普通变量。 当声明完应用需要的主要变量后,在viewDidLoad 方法中添加如下代码. override func viewDidLoad() { super.viewDidLoad() tapRec.addTarget(self, action: "tappedView") pinchRec.addTarget(self, action: "pinchedView:") swipeRec.addTarget(self, action: "swipedView") longPressRec.addTarget(self, action: "longPressedView") rotateRec.addTarget(self, action: "rotatedView:") panRec.addTarget(self, action: "draggedView:") tapView.addGestureRecognizer(tapRec) swipeView.addGestureRecognizer(swipeRec) pinchView.addGestureRecognizer(pinchRec) longPressView.addGestureRecognizer(longPressRec) rotateView.addGestureRecognizer(rotateRec) panView.addGestureRecognizer(panRec) rotateView.userInteractionEnabled = true rotateView.multipleTouchEnabled = true pinchView.userInteractionEnabled = true pinchView.multipleTouchEnabled = true tapView.userInteractionEnabled = true swipeView.userInteractionEnabled = true longPressView.userInteractionEnabled = true panView.userInteractionEnabled = true } 第 3 – 8行,为每个视图设定手势识别的目标。所谓的目标,就是每个view中的手势完成后要调用的方法。 第 9 -14行,把手势识别添加到视图中. 第15 – 22行,把每个视图的 userInteractionEnabled 属性设为ture,并把拥有需要多点触控(rotateView and pinchView)的手势所在的视图的multipleTouchEnabled 属性设为true. 现在,我们编写每个手势识别器要调用的方法 (第3 – 8行设置的目标方法 ). 添加如下代码: func tappedView(){ let tapAlert = UIAlertController(title: "Tapped", message: "You just tapped the tap view", preferredStyle: UIAlertControllerStyle.Alert) tapAlert.addAction(UIAlertAction(title: "OK", style: .Destructive, handler: nil)) self.presentViewController(tapAlert, animated: true, completion: nil) } func swipedView(){ let tapAlert = UIAlertController(title: "Swiped", message: "You just swiped the swipe view", preferredStyle: UIAlertControllerStyle.Alert) tapAlert.addAction(UIAlertAction(title: "OK", style: .Destructive, handler: nil)) self.presentViewController(tapAlert, animated: true, completion: nil) } func longPressedView(){ let tapAlert = UIAlertController(title: "Long Pressed", message: "You just long pressed the long press view", preferredStyle: UIAlertControllerStyle.Alert) tapAlert.addAction(UIAlertAction(title: "OK", style: .Destructive, handler: nil)) self.presentViewController(tapAlert, animated: true, completion: nil) } 这三种方法都很好地完成同一件事.每次在手势在相应的视图中完成后,每种方法都弹出一个对话框. 所以 tappedView() 方法在用户滑动视图时弹出一个对话框,swipedView() 方法在用户触摸滑动 swipe视图时弹出对话框,而longPressedView() 方法则在用户长按long press view时弹出对话框. 另两种手势 (rotate and pinch ) 的代码稍微有点复杂. 为旋转手势添加如下代码: func rotatedView(sender:UIRotationGestureRecognizer){ var lastRotation = CGFloat() self.view.bringSubviewToFront(rotateView) if(sender.state == UIGestureRecognizerState.Ended){ lastRotation = 0.0; } rotation = 0.0 - (lastRotation - sender.rotation) var point = rotateRec.locationInView(rotateView) var currentTrans = sender.view.transform var newTrans = CGAffineTransformRotate(currentTrans, rotation) sender.view.transform = newTrans lastRotation = sender.rotation } 这个方法包含 sender:UIRotationGestureRecognizer 参数. sender 参数( UIRotationGestureRecognizer 类型) 含有这个方法(在这个案例中是rotateRec)调用的手势识别器的值. 第2行声明了 lastRotation. 第3行我们把 rotateView放到前面. 接下来,在 if语句中,我们检查手势是否完成,如果没有完成,我们就将视图旋转。 第 8 – 10行,我们计算rotate view的旋转程度,第10行,我们设置rotate view的旋转程度。 On line 12 we set the lastRotation 作为旋转手势识别器的当前旋转. 现在我们添加pinch 手势的代码: func pinchedView(sender:UIPinchGestureRecognizer){ self.view.bringSubviewToFront(pinchView) sender.view.transform = CGAffineTransformScale(sender.view.transform, sender.scale, sender.scale) sender.scale = 1.0 } 在之前方法的第1行中,我们把pinch视图放到了顶端。然后设置每个pinch视图的transform,并把pinchRec的scale设为1. 然后是实现 pan (drag) 手势. 添加如下代码: func draggedView(sender:UIPanGestureRecognizer){ self.view.bringSubviewToFront(sender.view) var translation = sender.translationInView(self.view) sender.view.center = CGPointMake(sender.view.center.x + translation.x, sender.view.center.y + translation.y) sender.setTranslation(CGPointZero, inView: self.view) } 第2行,我们把 drag视图放到顶端 (和前面的方法一样). 然后我们声明变量translation,并用 sender.translationInView(self.view)的值给它赋值。 完成后,把sender.view object (panRec) 的center属性设为计算出来的新center ( 通过CGPointMake(sender.view.center.x + translation.x, sender.view.center.y + translation.y) 计算) 并把translation 设为 sender (panRec). 现在,代码部分算是完成了! 回到界面设计. 现在我们回到 “Main.storyboard” 文件. 选择视图控制器并把声明的每个UIView连接到相应的视图,如下图所示. 完工 现在你可以在模拟器或你的设备上运行该应用并测试手势。 后记 我希望这篇教程对你有所帮助。你可以在下载完整源代码,另外如果有什么问题,可以通过 Twitter 联系我. 最后,请在psdapps.gr上试用我的IOS应用.

优秀的个人博客,低调大师

Jenkins 如何迁移 Zadig,并发执行效率提升 100%

我们在「不想放弃 Jenkins?这么做也能云原生」一文中详细描述了如何在保留 Jenkins 的前提下,通过 Zadig 快速提升效率和工程师幸福度。然而,尽管这样做可以取得一些显著的成果,却未能实质解决运维人员对系统维护的繁重负担。实际情况中,Jenkins 的管理和维护存在诸如插件兼容性、内存泄漏、用户权限管理、脚本维护等多方面的问题,导致运维人员仍需花费大量时间进行系统维护。因此,是否可以完全弃用 Jenkins,并将现有任务全部迁移到 Zadig 上执行呢?答案是肯定的。Zadig 不仅具备 Jenkins 的全部功能,而且能够实现软件开发过程中复杂流程的自动化。 一、Zadig 工作流到底有何独特之处? Zadig 工作流引擎起初基于 Kubernetes 原生能力搭建,借助 Kubernertes 的资源动态分配能力,实现多任务的并发执行,相比 Jenkins 至少可以节省50%的资源,并可以提高至少40%的任务执行效率。 Zadig 工作流的设计更贴合实际业务场景,支持编排产品交付过程中涉及到的任何系统和工具,如:项目管理系统、代码托管平台、测试平台、部署工具、配置管理工具、数据管理工具、审批系统、企业自建系统等等。Zadig 工作流除了具备 CI 工作流的基本能力(比如克隆代码、执行 shell 脚本、触发器、通知、缓存等等)外,支持以下更多能力: 支持多服务共享构建、构建模板、利用 Serverless 资源构建 支持多服务的并发构建、并发部署、并发测试 支持项目管理中的任务状态变更、配置变更、数据变更 支持蓝绿发布、金丝雀发布、分批次灰度发布、MSE 全链路和 Istio 全链路发布 支持发布过程审批 在执行时支持根据实际的分支策略,自由选择Branch、PR/MR、Branch+PR/MR、Tag、Commit方式进行构建 ...... 工作流实现方式的细节差异 工作流关键环节 Jenkins Zadig 执行环境 手工制作环境 可扩展云原生环境及依赖包 代码信息 分散配置代码源 统一管理多种代码来源 执行脚本与变量 分散编写脚本 统一配置脚本规范 定时触发 定时触发 多种可定制触发策略 代码变更触发 插件代码触发 海量多种触发策略 工作流间的串接 根据工作流状态触发 服务化灵活编排调度 多任务并发执行 编写脚本控制并发 云原生任务GUI 配置并发 任务并发数量控制 资源节点控制并发 统一管理并发调度策略 二、如何将 Jenkins 上的配置迁移到 Zadig 上 下面详细介绍如何将已经在 Jenkins 上的相应配置迁移到 Zadig 上,按照不同的阶段拆解迁移的过程。 比较一:执行环境 对于工作流任务依赖的环境,在 Jenkins 上需在对应节点上手工制作,而在 Zadig 上支持管理任务运行时基础环境和依赖的软件包,方便平台运维统一管控业务构建、测试等过程使用的基础资源,保障资源的安全及合规。 Jenkins 任务的执行环境通过在配置中选择运行节点来指定,任务执行过程中用到的软件包需要在对应节点上安装和管理。 Zadig 任务的执行环境通过在配置中选择操作系统和依赖软件包来指定。 比较二:代码信息 对企业内部使用的代码源,在Jenkins上将其分散在不同的任务中进行管理,而在 Zadig 上由管理员统一集成,以确保代码源的安全性。 下面以 GitLab 为例,比对 Jenkins 和 Zadig 上代码信息的配置。 Jenkins 通过配置「源码管理」来实现构建代码源的定义。 Zadig 支持 GitLab、GitHub、Gerrit、Gitee 、其他通用 Git 代码源等代码托管平台的集成,完成集成后可列出代码库中有权限的代码仓库信息,包括 Branch、PR/MR、Tag 等等,对于开发者更加直观、体验更友好。 ·步骤 1:集成代码源。具体过程参考GitLab 代码源集成[1] ·步骤 2:任务中配置代码信息。Zadig 构建、测试、代码扫描及通用任务均支持拉取代码信息。 比较三:执行脚本及变量 对于服务执行脚本和变量的定义,在 Jenkins 上分散在各个任务中进行管理,而在 Zadig 上可以通过构建模版来标准化服务的构建过程,降低运维管理的负担。 下面以一个多服务的代码仓库的构建并推送镜像为例,比较 Jenkins 脚本编写和 Zadig 脚本编写的差异。 Jenkins 执行脚本及变量如下图所示,脚本中主要进行服务构建、镜像构建以及镜像推送过程。其中 $SERVICE、$VERSION、$PWD 变量需要在配置中定义。 Zadig 执行脚本及变量如下图所示,Zadig 构建内置 $SERVICE、$IMAGE 变量,脚本更加简洁。 两者之间的差异: 1. Zadig 任务执行过程中根据工作流配置的镜像仓库自动完成docker login操作,所以无需在脚本中声明。 2. 在 Zadig 中镜像命名规则支持统一配置和管理,具体可参考文档[2],所以无需在脚本中定义IMAGE变量的生成规则。 比较四:定时触发 工作流任务的定时执行场景比较常见,Jenkins 针对工作流任务的默认参数可以配置定时触发,而 Zadig 上除了可以指定触发时间周期外,还支持配置任务的执行变量,更加灵活。 Jenkins 触发器支持配置 Cron 表达式来定时触发任务。 Zadig 定时器支持多种触发方式,包括定时循环、间隔循环和 Cron 表达式,以满足各种定时触发的需求。此外,相较于 Jenkins 使用默认参数执行,Zadig 定时器允许配置不同的工作流执行变量,提供更灵活的定制选项。 比较五:代码变更触发 开发者提交代码自动触发工作流执行是持续集成和持续部署(CI/CD)中常见的实践。在 Jenkins 中,为实现这一需求,需要依赖插件。相比之下,Zadig 则内建 Git 触发器功能,无需额外插件,通过灵活的配置满足各种触发场景,从而提升整体效率。 Jenkins 可以通过安装插件实现代码变更触发任务的执行。 Zadig Git 触发器支持代码变更触发,通过定义代码信息、触发事件、代码文件目录以及工作流执行变量,来配置触发规则。这使得在代码库发生变更时,可以灵活而精准地触发相应的工作流,以满足各种复杂的自动化流程的执行。 除了上述两种触发器,Zadig 还支持多种其他触发器,包括「JIRA 触发器」、「飞书项目触发器」和「通用触发器」等,使用详情参考文档[3]。 比较六:工作流之间的串接编排 企业内部对于一些服务化的任务,例如安全扫描服务,需要进行统一管理并在多个工作流中使用。通常,这些任务由安全部门或平台团队进行统一管理,然后在各个业务工作流中进行调用。为了降低实施和后续维护的负担,一般选择采用多工作流串接的方式,以实现更高效的任务调度和管理。 Jenkins 通过配置「构建其他工程」来触发其他任务。 Zadig 的工作流本身采用了服务化的设计,使得测试、代码扫描等配置可以实现集中化的管理,然后轻松挂接到各个工作流中使用。这种设计使得配置和管理变得更加高效,同时在不同的工作流中灵活地应用这些服务,提高了整体工作流的可维护性和可扩展性。 比较七:多任务并发执行 多任务并发执行在复杂的软件开发流程、持续集成和部署中尤为关键。这能够显著减少工程师的等待时间,提高整体研发效率,从而加速项目进程,更灵活地应对不断变化的需求。 Jenkins 流水线支持不同的 "stage" 并发执行,详细配置请参考以下结构。 Zadig 工作流仅需在「阶段」上打开「并发执行」的开关,即可实现阶段内多个任务的并发执行。 比较八:任务并发数量控制 Jenkins 和 Zadig 均支持同一工作流的多个任务并发执行。Jenkins 通过资源节点来控制并发数量,而 Zadig 则统一管理并发调度策略,具有灵活控制任务优先级能力。 Jenkins 通过在节点上配置「任务执行数量」来控制多个任务的并发,单个 Jenkins 任务的并发可以在任务配置中指定。 Zadig 通过在任务配置中修改「任务并发数量设置」实现并发数控制,其中「工作流任务并发数量」控制同时执行的工作流任务数,「单任务服务并发数」控制同一个工作流任务中服务的并发数量。除此之外,面对低优先级任务占用全局并发数量的场景,可以通过配置工作流的「执行并发数」来解决。Zadig 具有更自由的任务并发数控制,能够灵活应对企业内部复杂的任务并发场景。 除以上能力外,Jenkins 通过插件来扩展更多的能力,而 Zadig 可以通过开发「自定义任务」,和企业自建系统打通,来满足企业复杂流程,具体开发过程参考文档[4]。 参考链接 [1]https://docs.koderover.com/zadig/settings/codehost/gitlab/ [2]https://docs.koderover.com/zadig/project/service/k8s/#策略配置 [3]https://docs.koderover.com/zadig/project/workflow-trigger/ [4]https://docs.koderover.com/zadig/settings/custom-task/ 立即体验 Zadig V2.0 新架构,开启高效交付之旅!🚀 Zadig 开放,链接,专业 Zadig 在 Github/Zadig 在 Gitee 推荐阅读: 是时候和 Jenkins 说再见了 不想放弃 Jenkins?这么做也能云原生 Zadig 推出环境睡眠,平均节省一半测试资源 Zadig vs. Jenkins 详细比对:时代的选择与开发者之选

优秀的个人博客,低调大师

100行代码提升10倍的性能

提出问题 从一个我常用的面试题,也是真实需求开始聊起: 你需要在前端展示 5000 条甚至更多的数据,每一条数据的数据结构是一个对象,里面有格式各样的属性。每个属性的值又可以是基本类型,对象,甚至数组。这里的对象或者数组内部的元素又可以继续包含对象或者数组并且允许无限嵌套下去。比如 { "name": { "firstName": "yi", "lastName": "li" }, "age": 23, "roles": ['developer', 'admin'], "projects": [{ "name": "demo", "repo": "" }] } 复制代码 页面上提供一个搜索框,用户通过输入搜索的内容可以找到包含这个内容的数据。注意,只要任意数据对象的任意属性值 (比如在上面的数据结构中,只要 name, age, roles 任何一个属性的值)包含这个关键词即可。如果属性值是数组或者对象,那么数组的元素或者对象的值继续对输入内容进行匹配检测,并递归的检测下去,只要有命中,便算该数据匹配 如何设计这个功能,让搜索功能尽可能的快? 解决思路 如果你稍有程序员的敏感度,此时你的脑海里应该有两个念头: 遍历以及深度优先遍历是最直接的方式 如果要求够快的话遍历我就输了 的确,遍历是最简单但也是最慢的。所以通常的优化方法之一是通过空间换取时间;而另一个方法……稍后再引出。 这里我们尝试通过建立字典树(Trie)来优化搜索。 如果你还不了解什么是字典树,下面做简单的介绍:假设我们有一个简单的对象,键值的对应关系如下: 我们根据「键」的字母出现顺次构建出一棵树出来,叶子节点值即有可能是某个「键」的值 那么此时无论用户想访问任何属性的值,只要从树的根节点出发,依据属性字母出现的顺序访问树的叶子节点,即可得到该属性的值。比如当我们想访问tea时: 但是在我们需要解决的场景中,我们不需要关心「属性」,我们只关心「值」是否匹配上搜索的内容。所以我们只需要对「值」建立字典树。 假设有以下的对象值 const o = { message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', } 复制代码 建立的树状结构如下: root--a |--c |--k |--p |--p |--l |--e |--n |--n |--a 复制代码 当用户搜索 apple 时,从a开始访问,至最后访问到字母 e 时,若在树中有对应的节点,表示命中;当用户搜索 aha 时,在访问 h 时就已经无法在树中找到对应的节点了,表示该对象不符合搜索条件 但实际工作中我们会有非常多个对象值,多个对象值之间可能有重复的值,所以匹配时,我们要把所有可能的匹配结果都返回。比如 [ { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', }, ] 复制代码 上面两个对象有相同的值 ack 和 an,所以在树上的叶子节点中我们还要添加对象的 id 辨识信息 root--a |--c |--k (ids: [1,2]) |--p |--p |--l |--e (ids: [1]) |--n (ids: [1, 2]) |--n |--a (ids: [1]) 复制代码 这样当用户搜索 an 时,我们能返回所有的匹配项 OK,有了思路之后我们开始实现代码。 代码实现 假数据 首先要解决的一个问题是如果快速的伪造 5000 条数据?这里我们使用 randomuser.me/api/ 开源 API。为了简单起见,我们让它只返回 gender, email, phone, cell, nat基本数据类型的值,而不返回嵌套结构(对象和数组)。注意这里只是为了便于代码展示和理解,略去了复杂的结构,也就避免了复杂的代码。加入复杂结构之后代码其实也没有大的变化,只是增加了遍历的逻辑和递归逻辑而已。 请求 randomuser.me/api/?result… 结果如下: { "results": [ { "gender": "male", "email": "enzo.dumont@example.com", "phone": "02-65-13-26-00", "cell": "06-09-02-19-99", "nat": "FR" }, { "gender": "male", "email": "gerald.omahony@example.com", "phone": "011-376-3811", "cell": "081-697-1414", "nat": "IE" } //... ] } 复制代码 叶子节点数据结构 根据思路中的描述,数据结构描述如下: class Leaf { constructor(id = "", value = "") { this.ids = id ? [id] : []; this.value = value; this.children = {}; } share(id) { this.ids.push(id); } } 复制代码 share方法用于向该叶子节点添加多个相同的匹配的id 帮助函数 在编码的过程中我们需要一些帮助函数,比如: isEmptyObject: 判断是否是空对象 distinct: 移除一个数组中的重复元素 这两个函数可以借用lodash类库实现,即使手动实现起来也很简单,这里就不赘述了 另一个重要的方法是normalize,我更习惯将normalize翻译为「扁平化」(而不是「标准化」),因为这样更形象。该方法用于将一个数组里的对象拆分为 id 与对象的映射关系。 比如将 [ { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', }, ] 复制代码 扁平化之后为 { '1': { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, '2': { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', } } 复制代码 之所以要这么做是为了当检索结果返回一个 id 数组时:[1, 2, 3],我们只需要遍历一边返回结果就能通过 id 在扁平化的 Map 里立即找到对应的数据。否则还要不停的遍历原始数据数组找到对应的数据. 因为 randomuser.me 返回的信息中不包含 id 信息,所以我们暂时用 email 信息作为唯一标示。normalize 的实现如下: function normalize(identify, data) { const id2Value = {}; data.forEach(item => { const idValue = item[identify]; id2Value[idValue] = item; }); return id2Value; } 复制代码 构建一棵树 这部分代码就没有什么秘密了,完全是按照递算法归构建一颗树了 fetch("https://randomuser.me/api/?results=5000&inc=gender,email,phone,cell,nat") .then(response => { return response.json(); }) .then(data => { const { results } = data; const root = new Leaf(); const identifyKey = "email"; results.forEach(item => { const identifyValue = item[identifyKey]; Object.values(item).forEach(itemValue => { // 注意这里会把 Number 和 Boolean 类型也字符串化 const stringifiedValue = String(itemValue); let tempRoot = root; const arraiedStringifiedValue = Array.from(stringifiedValue); arraiedStringifiedValue.forEach((character, characterIndex) => { const reachEnd = characterIndex === arraiedStringifiedValue.length - 1; if (!tempRoot.children[character]) { tempRoot.children[character] = new Leaf( reachEnd ? identifyValue : "", character ); tempRoot = tempRoot.children[character]; } else { if (reachEnd) { tempRoot.children[character].share(identifyValue); } tempRoot = tempRoot.children[character]; } }); }); }); 复制代码 模糊搜索 搜索部分代码也没有什么秘密,按图索骥而已: function searchBlurry(root, keyword, userMap) { const keywordArr = Array.from(String(keyword)); let tempRoot = root; let result = []; for (let i = 0; i < keywordArr.length; i++) { const character = keywordArr[i]; if (!tempRoot.children[character]) { break; } else { tempRoot = tempRoot.children[character]; } if (keywordArr.length - 1 === i) { result = [ ...tempRoot.ids, ...collectChildrenInsideIds(tempRoot.children) ]; } } return distinct(result).map(id => { return userMap[id]; }); } 复制代码 注意这里有一个collectChildrenInsideIds方法,这个方法用于收集该叶子节点下所有的子节点的 id。这么做是因为当前操作模糊匹配,当你搜索a时,apple, anna, ack 都算匹配。 常规搜索办法以及字典树的缺陷 为了对比效率,并且为了测试搜索结果的正确性,我们仍然需要编写一个常规的遍历的搜索方法: function regularSearch(searchKeyword) { const regularSearchResults = []; results.forEach(item => { for (const key in item) { const value = item[key]; if (String(value).startsWith(searchKeyword)) { regularSearchResults.push(item); break; } } }); return regularSearchResults } 复制代码 注意在测试对象值是否匹配搜索词时,我们使用了startsWith,而不是indexOf,**这是因为字典树的缺陷在于只能匹配以搜索词开头的词!**比如当你搜索a时,只能匹配apple、anna而不能匹配banana。为了便于对比,我们不得不使用startsWith 性能的对比 性能的对比结果是很有意思的: 当数据量较小时,查找效率不会有大的差异 当数据量较大时,比如 5000 条的情况下,当你的搜索词非常短小,比如a,那么字典树的查找效率会比遍历搜索低,也就是反而花费的时间长;当搜索词变得具体时,比如ali,字典树的查找效率会比遍历搜索高 效率反而低的问题不难想到是为什么:当你搜索词简单时,访问的叶子节点会少,所以只能扫描children收集子节点的所有的可能 id,这步操作中遍历的过程占用了大部分时间 但是我们仍然需要满足这部分的查询需求,所以我们要针对这个场景做一些优化 优化简短搜索的场景 我们回想一下简单搜索的场景,性能的瓶颈主要在于我们需要遍历叶子节点下的所有子节点。好办,鉴于树构建完之后不会再发生变化,那么我们只需要提前计算好每个叶子节点的所以子 id 就好了,这就是文章开头说的第二类优化方案,即预计算。 我编写了一个新的方法,用于递归的给每个叶子节点添加它所有子节点的 id: function decorateWithChildrenIds(root) { const { children } = root; root.childrenIds = collectChildrenInsideIds(root.children); for (const character in children) { const characterLeaf = children[character]; characterLeaf.childrenIds = collectChildrenInsideIds( characterLeaf.children ); decorateWithChildrenIds(characterLeaf); } } 复制代码 那么在构建完树之后,用这个方法把所有叶子节点「装饰」一遍就好了 结论 在通过预计算之后,在 5000 条数据的情况下,无论是短搜索还是长搜索,字典树的查找效率基本是在 1ms 左右,而常规的遍历查找则处于 10ms 左右,的确是十倍的提升。但是这个提升的代价是建立在牺牲空间,以及提前花费了时间计算的情况下。相信如果数据结构变得更复杂,效率提升会更明显 最后留下一个问题给大家:当需要搜寻的数据量变大时,比如 1000 时,偶尔会出现字典树搜索结果和遍历搜索结果不一致的情况,而当数据量变得更大时,比如 5000 条,那么这个「问题」会稳定出现。这个问题算不上 bug,但是问题出在哪呢 ? 如果觉得这篇文章还不错,来个分享、点赞吧,让更多的人也看到 如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu 不胜感激 ! 来自 “开源世界 ” ,链接: https://ym.baisou.ltd/post/756.html ,如需转载,请注明出处,否则将追究法律责任。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册