上一篇大致了解了docker 容器的创建过程,其实主要还是从文件系统的视角分析了创建一个容器时需要得建立 RootFS,建立volumes等步骤;本章来分析一下建立好一个容器后,将这个容器运行起来的过程,

本章主要分析一下 docker deamon端的实现方法;根据前面几章的介绍可以容易找到,客户端的实现代码在api/client/run.go中,大体步骤是首先通过上一篇文章中的createContainer()方法建立一个container,然后通过调用cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)来实现将这个container启动;在api/server/server.go中,客户端请求对应的mapping为 "/containers/{name:.*}/start":   s.postContainersStart,实现方法postContainerStart在api/server/container.go文件中,代码如下:

func (s *Server) postContainersStart(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

if vars == nil {

return fmt.Errorf("Missing parameter")

}

var hostConfig *runconfig.HostConfig

if r.Body != nil && (r.ContentLength > 0 || r.ContentLength == -1) {

if err := checkForJSON(r); err != nil {

return err

}

c, err := runconfig.DecodeHostConfig(r.Body)

if err != nil {

return err

}

hostConfig = c

}

if err := s.daemon.ContainerStart(vars["name"], hostConfig); err != nil {

if err.Error() == "Container already started" {

w.WriteHeader(http.StatusNotModified)

return nil

}

return err

}

w.WriteHeader(http.StatusNoContent)

return nil

}

逻辑非常简单,首先从request中解析参数,然后调用s.daemon.ContainerStart(vars["name"],hostConfig)启动容器,最后将结果写回response;主要的实现部分在s.daemon.ContainerStart(vars["name"],hostConfig)之中。在daemon/start.go中;

func (daemon *Daemon) ContainerStart(name string, hostConfig *runconfig.HostConfig) error {

container, err := daemon.Get(name)

if err != nil {

return err

}

if container.IsPaused() {

return fmt.Errorf("Cannot start a paused container, try unpause instead.")

}

if container.IsRunning() {

return fmt.Errorf("Container already started")

}

// Windows does not have the backwards compatibility issue here.

if runtime.GOOS != "windows" {

// This is kept for backward compatibility - hostconfig should be passed when

// creating a container, not during start.

if hostConfig != nil {

if err := daemon.setHostConfig(container, hostConfig); err != nil {

return err

}

}

} else {

if hostConfig != nil {

return fmt.Errorf("Supplying a hostconfig on start is not supported. It should be supplied on create")

}

}

// check if hostConfig is in line with the current system settings.

// It may happen cgroups are umounted or the like.

if _, err = daemon.verifyContainerSettings(container.hostConfig, nil); err != nil {

return err

}

if err := container.Start(); err != nil {

return fmt.Errorf("Cannot start container %s: %s", name, err)

}

return nil

}

首先根据传进来的名字,通过deamon.Get() (daemon/daemon.go)

func (daemon *Daemon) Get(prefixOrName string) (*Container, error) {

if containerByID := daemon.containers.Get(prefixOrName); containerByID != nil {

// prefix is an exact match to a full container ID

return containerByID, nil

}

// GetByName will match only an exact name provided; we ignore errors

if containerByName, _ := daemon.GetByName(prefixOrName); containerByName != nil {

// prefix is an exact match to a full container Name

return containerByName, nil

}

containerId, indexError := daemon.idIndex.Get(prefixOrName)

if indexError != nil {

return nil, indexError

}

return daemon.containers.Get(containerId), nil

}

首先从daemon.containers中根据name来进行查找,找出container是否已经存在了。daemon.container是contStore类型的结构体,其结构如下:

type contStore struct {

s map[string]*Container

sync.Mutex

}

接着通过GetByName查找:GetByName同样在daemon/daemon.go中,代码如下:

func (daemon *Daemon) GetByName(name string) (*Container, error) {

fullName, err := GetFullContainerName(name)

if err != nil {

return nil, err

}

entity := daemon.containerGraph.Get(fullName)

if entity == nil {

return nil, fmt.Errorf("Could not find entity for %s", name)

}

e := daemon.containers.Get(entity.ID())

if e == nil {

return nil, fmt.Errorf("Could not find container for entity id %s", entity.ID())

}

return e, nil

}

daemon.containerGraph是graphdb.Database类型(pkg/graphdb/graphdb.go文件中),

type Database struct {

conn *sql.DB

mux  sync.RWMutex

}

Database是一个存储容器和容器之间关系的数据库;目前Database是一个sqlite3数据库,所在的路径是/var/lib/docker/link/linkgraph.db中,其是在NewDaemon的实例化过程中,传递进来的。

graphdbPath := filepath.Join(config.Root, "linkgraph.db")

graph, err := graphdb.NewSqliteConn(graphdbPath)

if err != nil {

return nil, err

}

d.containerGraph = graph

数据库中最主要有两个表,分别是Entity,Edge,每一个镜像对应一个实体,存在Entity表;每个镜像与其父镜像的关系存在Edge表。每一个表在代码中也对应着一个结构体:

// Entity with a unique id.

type Entity struct {

id string

}

// An Edge connects two entities together.

type Edge struct {

EntityID string

Name     string

ParentID string

}

通过建表语句也许更能直观一些:

createEntityTable = `

CREATE TABLE IF NOT EXISTS entity (

id text NOT NULL PRIMARY KEY

);`

createEdgeTable = `

CREATE TABLE IF NOT EXISTS edge (

"entity_id" text NOT NULL,

"parent_id" text NULL,

"name" text NOT NULL,

CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"),

CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id")

);

`

最后一步就是通过GetByName查找完之后,接着根据daemon.idIndex.Get()进行查找,idIndex和前一篇中的镜像的idIndex是一样的,是一个trie的结构;

回到ContainerStart() 函数,在获取了container之后,接着判断container是否是停止和正在运行的,如果都不是, 在进行一些参数验证(端口映射的设置、验证exec driver、验证内核是否支持cpu share,IO weight等)后,则启动调用container.Start() (daemon/container.go)启动container;

func (container *Container) Start() (err error) {

container.Lock()

defer container.Unlock()

if container.Running {

return nil

}

if container.removalInProgress || container.Dead {

return fmt.Errorf("Container is marked for removal and cannot be started.")

}

// if we encounter an error during start we need to ensure that any other

// setup has been cleaned up properly

defer func() {

if err != nil {

container.setError(err)

// if no one else has set it, make sure we don't leave it at zero

if container.ExitCode == 0 {

container.ExitCode = 128

}

container.toDisk()

container.cleanup()

container.LogEvent("die")

}

}()

if err := container.Mount(); err != nil {

return err

}

// Make sure NetworkMode has an acceptable value. We do this to ensure

// backwards API compatibility.

container.hostConfig = runconfig.SetDefaultNetModeIfBlank(container.hostConfig)

if err := container.initializeNetworking(); err != nil {

return err

}

linkedEnv, err := container.setupLinkedContainers()

if err != nil {

return err

}

if err := container.setupWorkingDirectory(); err != nil {

return err

}

env := container.createDaemonEnvironment(linkedEnv)

if err := populateCommand(container, env); err != nil {

return err

}

mounts, err := container.setupMounts()

if err != nil {

return err

}

container.command.Mounts = mounts

return container.waitForStart()
}

defer func() 里面的作用就是如果start container出问题的话,进行一些清理工作;

container.Mount() 挂在container的aufs文件系统;

initializeNetworking() 对网络进行初始化,docker网络模式有三种,分别是 bridge模式(每个容器用户单独的网络栈),host模式(与宿主机共用一个网络栈),contaier模式(与其他容器共用一个网络栈,猜测kubernate中的pod所用的模式);根据config和hostConfig中的参数来确定容器的网络模式,然后调动libnetwork包来建立网络,关于docker网络的部分后面会单独拿出一章出来梳理;

container.setupLinkedContainers() 将通过--link相连的容器中的信息获取过来,然后将其中的信息转成环境变量(是[]string数组的形式,每一个元素类似于"NAME=xxxx")的形式

返回;

setupWorkingDirectory() 建立容器执行命令时的工作目录;

createDaemonEnvironment() 将container中的自有的一些环境变量和之前的linkedEnv和合在一起(append),然后返回;

populateCommand(container, env) 主要是为container的execdriver(最终启动容器的) 设置网络模式、设置namespace(pid,ipc,uts)等、资源(resources)限制等,并且设置在容器内执行的Command,Command中含有容器内进程的启动命令;

container.setupMounts() 返回container的所有挂载点;

最后调用container.waitForStart()函数启动容器;

func (container *Container) waitForStart() error {

container.monitor = newContainerMonitor(container, container.hostConfig.RestartPolicy)

// block until we either receive an error from the initial start of the container's

// process or until the process is running in the container

select {

case <-container.monitor.startSignal:

case err := <-promise.Go(container.monitor.Start):

return err

}

return nil

}

首先实例化出来一个containerMonitor,monitor的作用主要是监控容器内第一个进程的执行,如果执行没有成功,那么monitor可以按照一定的重启策略(startPolicy)来进行重启;

看下一下montitor(daemon/monitor.go)中的Start()函数,最主要的部分是

m.container.daemon.Run(m.container, pipes, m.callback)

在daemon/daemon.go文件中, Run方法:

func (daemon *Daemon) Run(c *Container, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (execdriver.ExitStatus, error) {
     return daemon.execDriver.Run(c.command, pipes, startCallback)
}

docker的execDriver有两个:lxc 和 native;lxc是较早的driver,native是默认的,用的是libcontainer;所以最终这个Run的方式是调用daemon/execdriver/native/driver.go中的Run() 方法:

func (d *Driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (execdriver.  ExitStatus, error) {

// take the Command and populate the libcontainer.Config from it

container, err := d.createContainer(c)

if err != nil {

return execdriver.ExitStatus{ExitCode: -1}, err

}

p := &libcontainer.Process{

Args: append([]string{c.ProcessConfig.Entrypoint}, c.ProcessConfig.Arguments...),

Env:  c.ProcessConfig.Env,

Cwd:  c.WorkingDir,

User: c.ProcessConfig.User,

}

if err := setupPipes(container, &c.ProcessConfig, p, pipes); err != nil {

return execdriver.ExitStatus{ExitCode: -1}, err

}

cont, err := d.factory.Create(c.ID, container)

if err != nil {

return execdriver.ExitStatus{ExitCode: -1}, err

}

d.Lock()

d.activeContainers[c.ID] = cont

d.Unlock()

defer func() {

cont.Destroy()

d.cleanContainer(c.ID)

}()

if err := cont.Start(p); err != nil {

return execdriver.ExitStatus{ExitCode: -1}, err

}

if startCallback != nil {

pid, err := p.Pid()

if err != nil {

p.Signal(os.Kill)

p.Wait()

return execdriver.ExitStatus{ExitCode: -1}, err

}

startCallback(&c.ProcessConfig, pid)

}

oom := notifyOnOOM(cont)

waitF := p.Wait

if nss := cont.Config().Namespaces; !nss.Contains(configs.NEWPID) {

// we need such hack for tracking processes with inherited fds,

// because cmd.Wait() waiting for all streams to be copied

waitF = waitInPIDHost(p, cont)

}

ps, err := waitF()

if err != nil {

execErr, ok := err.(*exec.ExitError)

if !ok {

return execdriver.ExitStatus{ExitCode: -1}, err

}

ps = execErr.ProcessState

}

cont.Destroy()

_, oomKill := <-oom

return execdriver.ExitStatus{ExitCode: utils.ExitStatus(ps.Sys().(syscall.WaitStatus)), OOMKilled: oomKill}, nil

}

d.createContainer(c) 根据command实例化出来一个container需要的配置;Capabilities、Namespace、Group、mountpoints等,首先根据模板生成固定的配置(daemon/execdriver/native/template/default_template.go),然后在根据command建立容器特定的namespace

接着实例化一个libcontainer.Process{},里面的Args参数就是用户输入的entrypoint和cmd参数的组合,这也是将来容器的第一个进程(initProcess)要运行的一部分;

setupPipes(container, &c.ProcessConfig, p, pipes); 将container类(pipes)的标准输入输出与 libcontainer.Process (也是将来容器中的的init processs,就是变量p)进行绑定,这样就可以获取初始进程的输入和输出;

cont, err := d.factory.Create(c.ID, container)  调用driver.factory(~/docker_src/vendor/src/github.com/opencontainers/runc/libcontainer/factory_linux.go )来实例化一个linux container,结构如下:

linuxContainer{

id:            id,

root:          containerRoot,

config:        config,

initPath:      l.InitPath,

initArgs:      l.InitArgs,

criuPath:      l.CriuPath,

cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),

}

这个linuxContainer类和之前的container类是不同的,这个是execdriver专有的类,其中比较主要的,ID就是containerID,initPath:是dockerinit的路径,initArgs是docker init的参数,然后是CriuPath(用于给容器做checkpoint),cgroupMangeer:管理容器的进程所在的资源;

dockerinit要说一下,dockerinit是一个固定的二进制文件,是一个容器运行起来之后去执行的第一个可执行文件,dockerinit的作用是在新的namespace中设置挂在资源,初始化网络栈等等,当然还有一作用是由dockerinit来负责执行用户设定的entrypoint和cmd;执行entrypoint和cmd,执行entrypoint和cmd的时候,与dockerinit是在同一个进程中;

cont.Start(p); 通过linuxcontainer运行之前的libcontainer.Process,这个步骤稍后会详细讲解;

接下来就是常规的步骤了,调用callback函数、监控container是否会有内存溢出的问题(通过cgroupmanager)、然后p.Wait()等待libcontainer.Process执行完毕、无误执行完毕后接着调用destroy销毁linuxcontainer,然后返回执行状态;

接下来对linuxcontainer的start(vendor/src/github.com/opencontainers/runc/libcontainer/container_linux.go)过程详细介绍一下;

func (c *linuxContainer) Start(process *Process) error {

c.m.Lock()

defer c.m.Unlock()

status, err := c.currentStatus()

if err != nil {

return err

}

doInit := status == Destroyed

parent, err := c.newParentProcess(process, doInit)

if err != nil {

return newSystemError(err)

}

if err := parent.start(); err != nil {

// terminate the process to ensure that it properly is reaped.

if err := parent.terminate(); err != nil {

logrus.Warn(err)

}

return newSystemError(err)

}

process.ops = parent

if doInit {

c.updateState(parent)

}

return nil

}

这个Start()函数的作用就是开启容器的第一个进程initProcess,docker daemon开启一个新的容器,其实就是fork出一个新的进程(这个进程有自己的namespace,从而实现容器间的隔离),这个进程同时也是容器的初始进程,这个初始进程用来执行dockerinit、entrypoint、cmd等一系列操作;

status, err := c.currentStatus() 首先判断一下容器的初始进程是否已经存在,不存在的话会返回destroyd状态;

parent, err := c.newParentProcess(process, doInit)  开启新的进程,下面插进来一下关于newParentProcess的代码

func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {

parentPipe, childPipe, err := newPipe()

if err != nil {

return nil, newSystemError(err)

}

cmd, err := c.commandTemplate(p, childPipe)

if err != nil {

return nil, newSystemError(err)

}

if !doInit {

return c.newSetnsProcess(p, cmd, parentPipe, childPipe), nil

}

return c.newInitProcess(p, cmd, parentPipe, childPipe)

}

func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {

cmd := &exec.Cmd{

Path: c.initPath,

Args: c.initArgs,

}

cmd.Stdin = p.Stdin

cmd.Stdout = p.Stdout

cmd.Stderr = p.Stderr

cmd.Dir = c.config.Rootfs

if cmd.SysProcAttr == nil {

cmd.SysProcAttr = &syscall.SysProcAttr{}

}

cmd.ExtraFiles = append(p.ExtraFiles, childPipe)

cmd.Env = append(cmd.Env, fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1))

if c.config.ParentDeathSignal > 0 {

cmd.SysProcAttr.Pdeathsig = syscall.Signal(c.config.ParentDeathSignal)

}

return cmd, nil

}

上面两个函数是相互关联的,上面的函数调用了下面的函数,

newParentProcess中首先调用了

parentPipe, childPipe, err := newPipe() 来创建一个socket pair,形成一个管道;这个管道是docker daemon 与 将来的dockerinit进行通信的渠道, 上面说过dockerinit的作用是初始化新的namespace 内的一些重要资源,但这些资源是需要docker daemon 在宿主机上申请的,如:veth pair,docker daemon 在自己的命名空间中创建了这些内容之后,通过这个管道将数据交给 dockerinit

接着cmd, err := c.commandTemplate(p, childPipe)。这部分主要有两个作用,将dockerinit及其参数分装成go语言中的exec.Cmd类,

&exec.Cmd{

Path: c.initPath,

Args: c.initArgs,

}

这个Cmd类就是将来要真正执行的进程;其他一些事情是绑定Cmd的表述输入输入到libcontainer.Process(之前已经将输入输出绑定到container类),还有将管道的childpipe一端绑定到Cmd类的打开的文件中。

接着在newParentProcess中,返回了 newInitProcess(p, cmd, parentPipe, childPipe),其实质是返回了一个initProcess类(vendor/src/github.com/opencontainers/runc/libcontainer/process_linux.go);

initProcess{

cmd:        cmd,

childPipe:  childPipe,

parentPipe: parentPipe,

manager:    c.cgroupManager,

config:     c.newInitConfig(p),

}

其中的cmd,就是之前封装好的exec.Cmd类、然后childPipe已经绑定到了cmd的文件描述符中、parentPipe是pipe的另一端、manager是cgroup控制资源的作用、config是将之前的libcontainer.Process的配置(其中包括entrypoint和cmd的配置)转化成一些配置信息,这部分配置信息将通过parentPipe发给cmd的childpipe,最终由dockerinit来运行、接下来会讲到;

然后回到 Start()函数中, parent就是一个initProcess类,紧接着就是调用这个类的start()方法了

func (p *initProcess) start() error {

defer p.parentPipe.Close()

err := p.cmd.Start()

p.childPipe.Close()

if err != nil {

return newSystemError(err)

}

fds, err := getPipeFds(p.pid())

if err != nil {

return newSystemError(err)

}

p.setExternalDescriptors(fds)

if err := p.manager.Apply(p.pid()); err != nil {

return newSystemError(err)

}

defer func() {

if err != nil {

// TODO: should not be the responsibility to call here

p.manager.Destroy()

}

}()

if err := p.createNetworkInterfaces(); err != nil {

return newSystemError(err)

}

if err := p.sendConfig(); err != nil {

return newSystemError(err)

}

// wait for the child process to fully complete and receive an error message

// if one was encoutered

var ierr *genericError

if err := json.NewDecoder(p.parentPipe).Decode(&ierr); err != nil && err != io.EOF {

return newSystemError(err)

}

if ierr != nil {

return newSystemError(ierr)

}

return nil

}

最主要的几个步骤,p.cmd.Start() 首先运行cmd的命令;

p.manager.Apply(p.pid()) cmd运行起来之后,是一个新的进程,也是container中的第一个进程,会有一个pid,将这个pid加入到cgroup配置中,确保以后由初始进程fork出来的子进程也能遵守cgroup的资源配置;

createNetworkInterfaces() 为进程建立网络配置,并放到config配置中;

p.sendConfig() 将配置(包括网络配置、entrypoint、cmd等)通过parentPipe发给cmd进程,并有cmd中的dockerinit执行;

json.NewDecoder(p.parentPipe).Decode(&ierr);  等待cmd的执行是否会有问题;

容器的启动主要过程就是 docker 将container的主要配置封装成一个Command类,然后交给execdriver(libcontainer),libcontainer将command中的配置生成一个libcontainer.process类和一个linuxcontainer类,然后由linux container这个类运行libcontainer.process。运行的过程是生成一个os.exec.Cmd类(里面包含dockerinit),启动这个dockerinit,然后在运行entrypoint和cmd;

年前就先分析这么多了,接下来要看看swarm、kubernates、和docker 网络相关的东西;

docker 源码分析 六(基于1.8.2版本),Docker run启动过程的更多相关文章

  1. Docker源码分析(六):Docker Daemon网络

    1. 前言 Docker作为一个开源的轻量级虚拟化容器引擎技术,已然给云计算领域带来了新的发展模式.Docker借助容器技术彻底释放了轻量级虚拟化技术的威力,让容器的伸缩.应用的运行都变得前所未有的方 ...

  2. Docker源码分析(八):Docker Container网络(下)

    1.Docker Client配置容器网络模式 Docker目前支持4种网络模式,分别是bridge.host.container.none,Docker开发者可以根据自己的需求来确定最适合自己应用场 ...

  3. Docker源码分析(五):Docker Server的创建

    1.Docker Server简介 Docker架构中,Docker Server是Docker Daemon的重要组成部分.Docker Server最主要的功能是:接受用户通过Docker Cli ...

  4. Docker源码分析(三):Docker Daemon启动

    1 前言 Docker诞生以来,便引领了轻量级虚拟化容器领域的技术热潮.在这一潮流下,Google.IBM.Redhat等业界翘楚纷纷加入Docker阵营.虽然目前Docker仍然主要基于Linux平 ...

  5. docker 源码分析 一(基于1.8.2版本),docker daemon启动过程;

    最近在研究golang,也学习一下比较火的开源项目docker的源代码,国内比较出名的docker源码分析是孙宏亮大牛写的一系列文章,但是基于的docker版本有点老:索性自己就git 了一下最新的代 ...

  6. docker 源码分析 四(基于1.8.2版本),Docker镜像的获取和存储

    前段时间一直忙些其他事情,docker源码分析的事情耽搁了,今天接着写,上一章了解了docker client 和 docker daemon(会启动一个http server)是C/S的结构,cli ...

  7. Docker源码分析(九):Docker镜像

    1.前言 回首过去的2014年,大家可以看到Docker在全球刮起了一阵又一阵的“容器风”,工业界对Docker的探索与实践更是一波高过一波.在如今的2015年以及未来,Docker似乎并不会像其他昙 ...

  8. Docker源码分析(七):Docker Container网络 (上)

    1.前言(什么是Docker Container) 如今,Docker技术大行其道,大家在尝试以及玩转Docker的同时,肯定离不开一个概念,那就是“容器”或者“Docker Container”.那 ...

  9. Docker源码分析(四):Docker Daemon之NewDaemon实现

    1. 前言 Docker的生态系统日趋完善,开发者群体也在日趋庞大,这让业界对Docker持续抱有极其乐观的态度.如今,对于广大开发者而言,使用Docker这项技术已然不是门槛,享受Docker带来的 ...

随机推荐

  1. jq异步上传文件(转载)

    最近在使用ajaxForm,随便把使用方法记下下来,以便以后回顾. 1 ,引入依赖脚本 <script type="text/JavaScript" src="/j ...

  2. SpringMVC之controller篇

    概述 继 spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能.现在你无须让 Controller 继承任何接口,无需在 ...

  3. .net 项目 调用webservice 出错,异常信息:对操作“xxx”的回复消息正文进行反序列化时出错。解决方案。

    项目运行好好的,增加并更新WebService后,出错,捕获异常信息为:对操作“xxx”的回复消息正文进行反序列化时出错.解决方案. 认真分析异常信息后,得到关键提醒: {"读取 XML 数 ...

  4. 即时聊天IM之二 openfire 整合现有系统用户

    合肥程序员群:49313181.    合肥实名程序员群:128131462 (不愿透露姓名和信息者勿加入) Q  Q:408365330     E-Mail:egojit@qq.com  综述: ...

  5. mac OS.NE开发环境搭建

    合肥程序员群:49313181.    合肥实名程序员群:128131462 (不愿透露姓名和信息者勿加入,申请备注填写姓名+技术+工作年限) Q  Q:408365330     E-Mail:eg ...

  6. Jmeter性能测试入门(链接收藏)

    Jmeter性能测试入门: http://www.cnblogs.com/TankXiao/p/4045439.html

  7. [转]搭建高可用mongodb集群(二)—— 副本集

    在上一篇文章<搭建高可用MongoDB集群(一)——配置MongoDB> 提到了几个问题还没有解决. 主节点挂了能否自动切换连接?目前需要手工切换. 主节点的读写压力过大如何解决? 从节点 ...

  8. sass中中文注释报错

    最近项目中用到了sass来编译css,但是scss代码中写了中文注释后编译报错, 经过查找文档和资料,终于找到了解决办法,即在scss文件顶部加上@charset "utf-8"; ...

  9. lex中yyrestart()的使用

    使用lex&yacc时,如果文件有错,parse停止. "每次调用yyparse(),语法分析器会忘记上次分析可能拥有的任何状态而重新开始分析.这不像lex产生的词法分析器的yyle ...

  10. css3圆角(还有百分比设置椭圆) 阴影 字体

    一. 现在前面只写知识点,全部内容放在后面截图展示: 给元素添加圆角: border-radius属性,他的值如果等于高的一半,将会等到一个圆形,大于一半叶然是一个圆形 radius是指半径,他求出圆 ...