精华内容
下载资源
问答
  • 图解kubernetes容器探活机制核心实现

    千次阅读 2020-02-11 12:07:06
    在k8s中通过kubelet拉起一个容器之后,用户可以指定探的方式用于实现容器的...探的线程模型设计相对简单一些,其通过worker来进行底层探任务的执行,并通过Manager来负责worker的管理, 同时缓存探的结果 1...

    在k8s中通过kubelet拉起一个容器之后,用户可以指定探活的方式用于实现容器的健康性检查,目前支持TCP、Http和命令三种方式,今天介绍其整个探活模块的实现, 了解其周期性探测、计数器、延迟等设计的具体实现

    1. 探活的整体设计

    1.1 线程模型

    image.png探活的线程模型设计相对简单一些,其通过worker来进行底层探活任务的执行,并通过Manager来负责worker的管理, 同时缓存探活的结果

    1.2 周期性探活

    image.png根据每个探活任务的周期,来生成定时器,则只需要监听定时器事件即可

    1.3 探活机制的实现

    image.png探活机制的实现除了命令Http和Tcp都相对简单,Tcp只需要直接通过net.DialTimeout链接即可,而Http则是通过构建一个http.Transport构造Http请求执行Do操作即可

    相对复杂的则是exec, 其首先要根据当前container的环境变量生成command,然后通过容器、命令、超时时间等构建一个Command最后才是调用runtimeService调用csi执行命令 

    2.探活接口实现

    2.1 核心成员结构

    type prober struct {
        exec execprobe.Prober
        // 我们可以看到针对readiness/liveness会分别启动一个http Transport来进行链接
        readinessHTTP httpprobe.Prober
        livenessHTTP  httpprobe.Prober
        startupHTTP   httpprobe.Prober
        tcp           tcpprobe.Prober
        runner        kubecontainer.ContainerCommandRunner
    
        // refManager主要是用于获取成员的引用对象
        refManager *kubecontainer.RefManager
        // recorder会负责探测结果事件的构建,并最终传递回 apiserver
        recorder   record.EventRecorder
    }

    2.2 探活主流程

    探活的主流程主要是位于prober的probe方法中,其核心流程分为三段

    2.2.1 获取探活的目标配置

    func (pb *prober) probe(probeType probeType, pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID) (results.Result, error) {
    var probeSpec *v1.Probe
    // 根据探活的类型来获取对应位置的探活配置
        switch probeType {
        case readiness:
            probeSpec = container.ReadinessProbe
        case liveness:
            probeSpec = container.LivenessProbe
        case startup:
            probeSpec = container.StartupProbe
        default:
            return results.Failure, fmt.Errorf("unknown probe type: %q", probeType)
        }

    2.2.2 执行探活记录错误信息

    如果返回的错误,或者不是成功或者警告的状态,则会获取对应的引用对象,然后通过 recorder进行事件的构造,发送结果返回apiserver

    // 执行探活流程    
    result, output, err := pb.runProbeWithRetries(probeType, probeSpec, pod, status, container, containerID, maxProbeRetries)
        
        if err != nil || (result != probe.Success && result != probe.Warning) {
            // // 如果返回的错误,或者不是成功或者警告的状态
            // 则会获取对应的引用对象,然后通过 
            ref, hasRef := pb.refManager.GetRef(containerID)
            if !hasRef {
                klog.Warningf("No ref for container %q (%s)", containerID.String(), ctrName)
            }
            if err != nil {
                klog.V(1).Infof("%s probe for %q errored: %v", probeType, ctrName, err)
                recorder进行事件的构造,发送结果返回apiserver
                if hasRef {
                    pb.recorder.Eventf(ref, v1.EventTypeWarning, events.ContainerUnhealthy, "%s probe errored: %v", probeType, err)
                }
            } else { // result != probe.Success
                klog.V(1).Infof("%s probe for %q failed (%v): %s", probeType, ctrName, result, output)
                // recorder进行事件的构造,发送结果返回apiserver
                if hasRef {
                    pb.recorder.Eventf(ref, v1.EventTypeWarning, events.ContainerUnhealthy, "%s probe failed: %s", probeType, output)
                }
            }
            return results.Failure, err
        }

    2.2.3 探活重试实现

    func (pb *prober) runProbeWithRetries(probeType probeType, p *v1.Probe, pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID, retries int) (probe.Result, string, error) {
        var err error
        var result probe.Result
        var output string
        for i := 0; i < retries; i   {
            result, output, err = pb.runProbe(probeType, p, pod, status, container, containerID)
            if err == nil {
                return result, output, nil
            }
        }
        return result, output, err
    }

    2.2.4 根据探活类型执行探活

    func (pb *prober) runProbe(probeType probeType, p *v1.Probe, pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID) (probe.Result, string, error) {
        timeout := time.Duration(p.TimeoutSeconds) * time.Second
        if p.Exec != nil {
            klog.V(4).Infof("Exec-Probe Pod: %v, Container: %v, Command: %v", pod, container, p.Exec.Command)
            command := kubecontainer.ExpandContainerCommandOnlyStatic(p.Exec.Command, container.Env)
            return pb.exec.Probe(pb.newExecInContainer(container, containerID, command, timeout))
        }
        if p.HTTPGet != nil {
            // 获取协议类型与 http参数信息
            scheme := strings.ToLower(string(p.HTTPGet.Scheme))
            host := p.HTTPGet.Host
            if host == "" {
                host = status.PodIP
            }
            port, err := extractPort(p.HTTPGet.Port, container)
            if err != nil {
                return probe.Unknown, "", err
            }
            path := p.HTTPGet.Path
            klog.V(4).Infof("HTTP-Probe Host: %v://%v, Port: %v, Path: %v", scheme, host, port, path)
            url := formatURL(scheme, host, port, path)
            headers := buildHeader(p.HTTPGet.HTTPHeaders)
            klog.V(4).Infof("HTTP-Probe Headers: %v", headers)
            switch probeType {
            case liveness:
                return pb.livenessHTTP.Probe(url, headers, timeout)
            case startup:
                return pb.startupHTTP.Probe(url, headers, timeout)
            default:
                return pb.readinessHTTP.Probe(url, headers, timeout)
            }
        }
        if p.TCPSocket != nil {
            port, err := extractPort(p.TCPSocket.Port, container)
            if err != nil {
                return probe.Unknown, "", err
            }
            host := p.TCPSocket.Host
            if host == "" {
                host = status.PodIP
            }
            klog.V(4).Infof("TCP-Probe Host: %v, Port: %v, Timeout: %v", host, port, timeout)
            return pb.tcp.Probe(host, port, timeout)
        }
        klog.Warningf("Failed to find probe builder for container: %v", container)
        return probe.Unknown, "", fmt.Errorf("missing probe handler for %s:%s", format.Pod(pod), container.Name)
    }

    3. worker工作线程

    Worker工作线程执行探测,要考虑几个问题:1.容器刚启动的时候可能需要等待一段时间,比如应用程序可能要做一些初始化的工作,还没有准备好2.如果发现容器探测失败后重新启动,则在启动之前重复的探测也是没有意义的3.无论是成功或者失败,可能需要一些阈值来进行辅助,避免单次小概率失败,重启容器

    3.1 核心成员 

    其中关键参数除了探测配置相关,则主要是onHold参数,该参数用于决定是否延缓对容器的探测,即当容器重启的时候,需要延缓探测,resultRun则是一个计数器,不论是连续成功或者连续失败,都通过该计数器累加,后续会判断是否超过给定阈值

    type worker struct {
        // 停止channel
        stopCh chan struct{}
    
        // 包含探针的pod
        pod *v1.Pod
    
        // 容器探针
        container v1.Container
    
        // 探针配置
        spec *v1.Probe
    
        // 探针类型
        probeType probeType
    
        // The probe value during the initial delay.
        initialValue results.Result
    
        // 存储探测结果
        resultsManager results.Manager
        probeManager   *manager
    
        // 此工作进程的最后一个已知容器ID。
        containerID kubecontainer.ContainerID
        // 最后一次探测结果
        lastResult results.Result
        // 探测连续返回相同结果的此时
        resultRun int
    
        // 探测失败会设置为true不会进行探测 
        onHold bool
    
        // proberResultsMetricLabels holds the labels attached to this worker
        // for the ProberResults metric by result.
        proberResultsSuccessfulMetricLabels metrics.Labels
        proberResultsFailedMetricLabels     metrics.Labels
        proberResultsUnknownMetricLabels    metrics.Labels
    }

    3.2 探测实现核心流程

    image.png

    3.2.1 失败容器探测中断

    如果当前容器的状态已经被终止了,则就不需要对其进行探测了,直接返回即可

        // 获取当前worker对应pod的状态
        status, ok := w.probeManager.statusManager.GetPodStatus(w.pod.UID)
        if !ok {
            // Either the pod has not been created yet, or it was already deleted.
            klog.V(3).Infof("No status for pod: %v", format.Pod(w.pod))
            return true
        }
        // 如果pod终止worker应该终止
        if status.Phase == v1.PodFailed || status.Phase == v1.PodSucceeded {
            klog.V(3).Infof("Pod %v %v, exiting probe worker",
                format.Pod(w.pod), status.Phase)
            return false
        }

    3.2.2 延缓探测恢复

    延缓探测恢复主要是指的在发生探测失败的情况下,会进行重启操作,在此期间不会进行探测,恢复的逻辑则是通过判断对应容器的id是否改变,通过修改onHold实现

    // 通过容器名字获取最新的容器信息    
    c, ok := podutil.GetContainerStatus(status.ContainerStatuses, w.container.Name)
        if !ok || len(c.ContainerID) == 0 {
            // Either the container has not been created yet, or it was deleted.
            klog.V(3).Infof("Probe target container not found: %v - %v",
                format.Pod(w.pod), w.container.Name)
            return true // Wait for more information.
        }
    
        if w.containerID.String() != c.ContainerID {
            // 如果容器改变,则表明重新启动了一个容器
            if !w.containerID.IsEmpty() {
                w.resultsManager.Remove(w.containerID)
            }
            w.containerID = kubecontainer.ParseContainerID(c.ContainerID)
            w.resultsManager.Set(w.containerID, w.initialValue, w.pod)
            // 获取到一个新的容器,则就需要重新开启探测 
            w.onHold = false
        }
    
        if w.onHold {
            //如果当前设置延缓状态为true,则不进行探测
            return true
        }

    3.2.3 初始化延迟探测

    初始化延迟探测主要是指的容器的Running的运行时间小于配置的InitialDelaySeconds则直接返回

        
    if int32(time.Since(c.State.Running.StartedAt.Time).Seconds()) < w.spec.InitialDelaySeconds {
            return true
        }

    3.2.4 执行探测逻辑

        result, err := w.probeManager.prober.probe(w.probeType, w.pod, status, w.container, w.containerID)
        if err != nil {
            // Prober error, throw away the result.
            return true
        }
    
        switch result {
        case results.Success:
            ProberResults.With(w.proberResultsSuccessfulMetricLabels).Inc()
        case results.Failure:
            ProberResults.With(w.proberResultsFailedMetricLabels).Inc()
        default:
            ProberResults.With(w.proberResultsUnknownMetricLabels).Inc()
        }

    3.2.5 累加探测计数

    在累加探测计数之后,会判断累加后的计数是否超过设定的阈值,如果未超过则不进行状态变更

        if w.lastResult == result {
            w.resultRun  
        } else {
            w.lastResult = result
            w.resultRun = 1
        }
    
        if (result == results.Failure && w.resultRun < int(w.spec.FailureThreshold)) ||
            (result == results.Success && w.resultRun < int(w.spec.SuccessThreshold)) {
            // Success or failure is below threshold - leave the probe state unchanged.
            // 成功或失败低于阈值-保持探测器状态不变。
            return true
        }

    3.2.6 修改探测状态

    如果探测状态发送改变,则需要先进行状态的保存,同时如果是探测失败,则需要修改onHOld状态为true即延缓探测,同时将计数器归0

    // 这里会修改对应的状态信息    
    w.resultsManager.Set(w.containerID, result, w.pod)
    
        if (w.probeType == liveness || w.probeType == startup) && result == results.Failure {
            // 容器运行liveness/starup检测失败,他们需要重启, 停止探测,直到有新的containerID
            // 这是为了减少命中#21751的机会,其中在容器停止时运行 docker exec可能会导致容器状态损坏
            w.onHold = true
            w.resultRun = 0
        }

    3.3 探测主循环流程

    主流程就很简答了执行上面的探测流程

    func (w *worker) run() {
        // 根据探活周期来构建定时器
        probeTickerPeriod := time.Duration(w.spec.PeriodSeconds) * time.Second
    
        // If kubelet restarted the probes could be started in rapid succession.
        // Let the worker wait for a random portion of tickerPeriod before probing.
        time.Sleep(time.Duration(rand.Float64() * float64(probeTickerPeriod)))
    
        probeTicker := time.NewTicker(probeTickerPeriod)
    
        defer func() {
            // Clean up.
            probeTicker.Stop()
            if !w.containerID.IsEmpty() {
                w.resultsManager.Remove(w.containerID)
            }
    
            w.probeManager.removeWorker(w.pod.UID, w.container.Name, w.probeType)
            ProberResults.Delete(w.proberResultsSuccessfulMetricLabels)
            ProberResults.Delete(w.proberResultsFailedMetricLabels)
            ProberResults.Delete(w.proberResultsUnknownMetricLabels)
        }()
    
    probeLoop:
        for w.doProbe() {
            // Wait for next probe tick.
            select {
            case <-w.stopCh:
                break probeLoop
            case <-probeTicker.C:
                // continue
            }
        }
    }
    

    今天就先到这里面,明天再聊proberManager的实现,大家分享转发,就算对我的支持了,动动手就绪

    微信号:baxiaoshi2020

    关注公告号阅读更多源码分析文章 21天大棚

    更多文章关注 www.sreguide.com

    本文由博客一文多发平台 OpenWrite 发布

    展开全文
  • 其实大部分问题我们之前也遇到过,这些问题当时也困扰着我们,后来我们经过讨论和思考,发现其实很时候我们困扰的主要原因是过于“追求完美的异地多活方案”,这样导致“异地多活”设计中出现很了的思维误区,而...

    其实大部分问题我们之前也遇到过,这些问题当时也困扰着我们,后来我们经过讨论和思考,发现其实很多时候我们困扰的主要原因是过于“追求完美的异地多活方案”,这样导致“异地多活”设计中出现很多了的思维误区,而如果不意识到这些思维误区,就会陷入死胡同,导致无法实现真正的“异地多活”方案。

    接下来我将总结常见的思维误区,看看你踩中了哪个坑?

    1所有业务异地多活

    “异地多活”是为了保证业务的高可用,但很多朋友在考虑这个“业务”的时候,会不自觉的陷入一个思维误区:我要保证所有业务的“异地多活”!

    比如说假设我们需要做一个“用户子系统”,这个子系统负责“注册”、“登录”、“用户信息”三个业务。为了支持海量用户,我们设计了一个“用户分区”的架构,即:正常情况下用户属于某个主分区,每个分区都有其它数据的备份,用户用邮箱或者手机号注册,路由层拿到邮箱或者手机号后,通过hash计算属于哪个中心,然后请求对应的业务中心。基本的架构如下:

    考虑这样一个系统,如果3个业务要同时实现异地多活,我们会发现如下一些难以解决的问题:

    注册

    A中心注册了用户,数据还未同步到B中心,此时A中心宕机,为了支持注册业务多活,那我们可以挑选B中心让用户去重新注册。看起来很容易就支持多活了,但仔细思考一下会发现这样做会有问题:一个手机号只能注册一个账号,A中心的数据没有同步过来,B中心无法判断这个手机号是否重复,如果B中心让用户注册,后来A中心恢复了,发现数据有冲突,怎么解决?

    实际上是无法解决的,因为注册账号不能说挑选最后一个生效;而如果B中心不支持本来属于A中心的业务进行注册,注册业务的双活又成了空谈。

    有的朋友可能会说:那我修改业务规则,允许一个手机号注册多个账号不就可以了么?

    这样做是不可行的,类似一个手机号只能注册一个账号这种规则,是核心业务规则,修改核心业务规则的代价非常大,几乎所有的业务都要重新设计,为了架构设计去改变业务规则,而且是这么核心的业务规则是得不偿失的。

    用户信息

    用户信息的修改和注册有类似的问题,即:A、B两个中心在异常的情况下都修改了用户信息,如何处理冲突?

    由于用户信息并没有账号那么关键,一种简单的处理方式是按照时间合并,即:最后修改的生效。业务逻辑上没问题,但实际操作也有一个很关键的坑:怎么保证多个中心所有机器时间绝对一致?在异地多中心的网络下,这个是无法保证的,即使有时间同步也无法完全保证,只要两个中心的时间误差超过1s,数据就可能出现混乱,即:先修改的反而生效。

    还有一种方式是生成全局唯一递增ID,这个方案的成本很高,因为这个全局唯一递增ID的系统本身又要考虑异地多活,同样涉及数据一致性和冲突的问题。

    综合上面的简单分析,我们可以发现,如果“注册”“登录”、“用户信息”全部都要支持异地多活的话,实际上是挺难的,有的问题甚至是无解的。那这种情况下我们应该如何考虑“异地多活”的方案设计呢?答案其实很简单:优先实现核心业务的异地多活方案!

    对于我们的这个模拟案例来说,“登录”才是最核心的业务,“注册”和“用户信息”虽然也是主要业务,但并不一定要实现异地多活。主要原因在于业务影响。对于一个日活1000万的业务来说,每天注册用户可能是几万,修改用户信息的可能还不到1万,但登录用户是1000万,很明显我们应该保证登录的异地多活。

    对于新用户来说,注册不了影响并不很明显,因为他还没有真正开始业务;用户信息修改也类似,用户暂时修改不了用户信息,对于其业务不会有很大影响,而如果有几百万用户登录不了,就相当于几百万用户无法使用业务,对业务的影响就非常大了:公司的客服热线很快就被打爆了,微博微信上到处都在传业务宕机,论坛里面到处是在骂娘的用户,那就是互联网大事件了!

    而登录实现“异地多活”恰恰是最简单的,因为每个中心都有所有用户的账号和密码信息,用户在哪个中心都可以登录。用户在A中心登录,A中心宕机后,用户到B中心重新登录即可。

    有的朋友可能会问,如果某个用户在A中心修改了密码,此时数据还没有同步到B中心,用户到B中心登录是无法登录的,这个怎么处理?这个问题其实就涉及另外一个思维误区了,我们稍后再谈。

    2实时一致性

    异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活设计方案的核心,但我们大部分人在考虑数据同步方案的时候,也会不知不觉的陷入完美主义误区:我要所有数据都实时同步!

    数据冗余就要将数据从A地同步到B地,从业务的角度来看是越快越好,最好和本地机房一样的速度最好,但让人头疼的问题正在这里:异地多活理论上就不可能很快,因为这是物理定律决定的,即:光速真空传播是每秒30万公里,在光纤中传输的速度大约是每秒20万公里,再加上传输中的各种网络设备的处理,实际还远远达不到光速的速度。

    除了距离上的限制外,中间传输各种不可控的因素也非常多,例如挖掘机把光纤挖断,中美海底电缆被拖船扯断、骨干网故障等,这些故障是第三方维护,我们根本无能为力也无法预知。例如广州机房到北京机房,正常情况下RTT大约是50ms左右,遇到网络波动之类的情况,RTT可能飙升到500ms甚至1s,更不用说经常发生的线路丢包问题,那延迟可能就是几秒几十秒了。

    因此异地多活方案面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。

    既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考:

    1. 尽量减少异地多活机房的距离,搭建高速网络;

    2. 尽量减少数据同步;

    3. 保证最终一致性,不保证实时一致性;

    减少距离:同城多中心

    为了减少两个业务中心的距离,选择在同一个城市不同的区搭建机房,机房间通过高速网络连通,例如在北京的海定区和通州区各搭建一个机房,两个机房间采用高速光纤网络连通,能够达到近似在一个机房的性能。

    这个方案的优势在于对业务几乎没有影响,业务可以无缝的切换到同城多中心方案;缺点就是无法应对例如新奥尔良全城被水淹,或者2003美加大停电这种极端情况。所以即使采用这种方案,也还必须有一个其它城市的业务中心作为备份,最终的方案同样还是要考虑远距离的数据传输问题。

    减少数据同步

    另外一种方式就是减少需要同步的数据。简单来说就是不重要的数据不要同步,同步后没用的数据不同步。

    以前面的“用户子系统”为例,用户登录所产生的token或者session信息,数据量很大,但其实并不需要同步到其它业务中心,因为这些数据丢失后重新登录就可以了。

    有的朋友会问:这些数据丢失后要求用户重新登录,影响用户体验的呀!

    确实如此,毕竟需要用户重新输入账户和密码信息,或者至少要弹出登录界面让用户点击一次,但相比为了同步所有数据带来的代价,这个影响完全可以接受,其实这个问题也涉及了一个异地多活设计的典型思维误区,后面我们会详细讲到。

    保证最终一致性

    第三种方式就是业务不依赖数据同步的实时性,只要数据最终能一致即可。例如:A机房注册了一个用户,业务上不要求能够在50ms内就同步到所有机房,正常情况下要求5分钟同步到所有机房即可,异常情况下甚至可以允许1小时或者1天后能够一致。

    最终一致性在具体实现的时候,还需要根据不同的数据特征,进行差异化的处理,以满足业务需要。例如对“账号”信息来说,如果在A机房新注册的用户5分钟内正好跑到B机房了,此时B机房还没有这个用户的信息,为了保证业务的正确,B机房就需要根据路由规则到A机房请求数据(这种处理方式其实就是后面讲的“二次读取”)。

    而对“用户信息”来说,5分钟后同步也没有问题,也不需要采取其它措施来弥补,但还是会影响用户体验,即用户看到了旧的用户信息,这个问题怎么解决呢?这个问题实际上也涉及到了一个思维误区,在最后我们统一分析。

    3只使用存储系统的同步功能

    数据同步是异地多活方案设计的核心,幸运的是基本上存储系统本身都会有同步的功能,例如MySQL的主备复制、Redis的Cluster功能、elasticsearch的集群功能。这些系统本身的同步功能已经比较强大,能够直接拿来就用,但这也无形中将我们引入了一个思维误区:只使用存储系统的同步功能!

    既然说存储系统本身就有同步功能,而且同步功能还很强大,为何说只使用存储系统是一个思维误区呢?因为虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。

    以MySQL为例,MySQL5.1版本的复制是单线程的复制,在网络抖动或者大量数据同步的时候,经常发生延迟较长的问题,短则延迟十几秒,长则可能达到十几分钟。而且即使我们通过监控的手段知道了MySQL同步时延较长,也难以采取什么措施,只能干等。

    Redis又是另外一个问题,Redis 3.0之前没有Cluster功能,只有主从复制功能,而为了设计上的简单,Redis主从复制有一个比较大的隐患:从机宕机或者和主机断开连接都需要重新连接主机,重新连接主机都会触发全量的主从复制,这时候主机会生成内存快照,主机依然可以对外提供服务,但是作为读的从机,就无法提供对外服务了,如果数据量大,恢复的时间会相当的长。

    综合上述的案例可以看出,存储系统本身自带的同步功能,在某些场景下是无法满足我们业务需要的。尤其是异地多机房这种部署,各种各样的异常都可能出现,当我们只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。

    解决的方案就是拓开思路,避免只使用存储系统的同步功能,可以将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案。

    例如,还是以前面的“用户子系统”为例,我们可以采用如下几种方式同步数据:

    1. 消息队列方式:对于账号数据,由于账号只会创建,不会修改和删除(假设我们不提供删除功能),我们可以将账号数据通过消息队列同步到其它业务中心。

    2. 二次读取方式:某些情况下可能出现消息队列同步也延迟了,用户在A中心注册,然后访问B中心的业务,此时B中心本地拿不到用户的账号数据。为了解决这个问题,B中心在读取本地数据失败的时候,可以根据路由规则,再去A中心访问一次(这就是所谓的二次读取,第一次读取本地,本地失败后第二次读取对端),这样就能够解决异常情况下同步延迟的问题。

    3. 存储系统同步方式:对于密码数据,由于用户改密码频率较低,而且用户不可能在1s内连续改多次密码,所以通过数据库的同步机制将数据复制到其它业务中心即可,用户信息数据和密码类似。

    4. 回源读取方式:对于登录的session数据,由于数据量很大,我们可以不同步数据;但当用户在A中心登录后,然后又在B中心登录,B中心拿到用户上传的session id后,根据路由判断session属于A中心,直接去A中心请求session数据即可,反之亦然,A中心也可以到B中心去拿取session数据。

    5. 重新生成数据方式:对于第4中场景,如果异常情况下,A中心宕机了,B中心请求session数据失败,此时就只能登录失败,让用户重新在B中心登录,生成新的session数据。

    (注意:以上方案仅仅是示意,实际的设计方案要比这个复杂一些,还有很多细节要考虑)

    综合上述的各种措施,最后我们的“用户子系统”同步方式整体如下:


    4100%可用性

    前面我们在给出每个思维误区对应的解决方案的时候,其实都遗留了一些小尾巴:某些场景下我们无法保证100%的业务可用性,总是会有一定的损失。例如密码不同步导致无法登录、用户信息不同步导致用户看到旧的用户信息等等,这个问题怎么解决?

    其实这个问题涉及异地多活设计方案中一个典型的思维误区:我要保证业务100%可用!但极端情况下就是会丢一部分数据,就是会有一部分数据不能同步,怎么办呢,有没有什么巧妙和神通的办法能做到?

    很遗憾,答案是没有!异地多活也无法保证100%的业务可用,这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法100%解决的。所以针对这个思维误区,我的答案是“忍”!也就是说我们要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的0.01%的用户的可用性,做个完美方案,结果却发现99.99%的用户都保证不了了。

    对于某些实时强一致性的业务,实际上受影响的用户会更多,甚至可能达到1/3的用户。以银行转账这个业务为例,假设小明在北京XX银行开了账号,如果小明要转账,一定要北京的银行业务中心是可用的,否则就不允许小明自己转账。

    如果不这样的话,假设在北京和上海两个业务中心实现了实时转账的异地多活,某些异常情况下就可能出现小明只有1万存款,他在北京转给了张三1万,然后又到上海转给了李四1万,两次转账都成功了。这种漏洞如果被人利用,后果不堪设想。

    当然,针对银行转账这个业务,可以有很多特殊的业务手段来实现异地多活。例如分为“实时转账”和“转账申请”。

    实时转账就是我们上述的案例,是无法做到“异地多活”的;但“转账申请”是可以做到“异地多活”的,即:小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求,然后后台异步发起真正的转账操作,如果此时北京业务中心不可用,转账请求就可以继续等待重试;假设等待2个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。

    小明再登录上来就会看到转账申请失败,原因是“余额不足”。不过需要注意的是“转账申请”的这种方式虽然有助于实现异地多活,但其实还是牺牲了用户体验的,对于小明来说,本来一次操作的事情,需要分为两次:一次提交转账申请,另外一次要确认是否转账成功。

    虽然我们无法做到100%可用性,但并不意味着我们什么都不能做,为了让用户心里更好受一些,我们可以采取一些措施进行安抚或者补偿,例如:

    1. 挂公告:说明现在有问题和基本的问题原因,如果不明确原因或者不方便说出原因,可以说“技术哥哥正在紧急处理”比较轻松和有趣的公告。

    2. 事后对用户进行补偿:例如送一些业务上可用的代金券、小礼包等,降低用户的抱怨。

    3. 补充体验:对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。以“转账申请”为例,为了让用户不用确认转账申请是否成功,我们可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果,这样用户就不用不时的登录系统来确认转账是否成功了。

    5一句话谈“异地多活”

    综合前面的分析,异地多活设计的理念可以总结为一句话:采用多种手段,保证绝大部分用户的核心业务异地多活!

    展开全文
  • 异地多活的设计思路

    千次阅读 2020-05-30 12:53:32
    异地多活是系统高可用的一种解决方案,它通过在个不同机房建立个数据中心,并且使这个数据中心都可以同时在线提供服务来避免当出现机房断电、光纤被挖断等场景出现服务不可用的场景,实现服务高可用,同时这...

    一. 什么是异地多活

    异地多活是系统高可用的一种解决方案,它通过在多个不同机房建立多个数据中心,并且使这多个数据中心都可以同时在线提供服务来避免当出现机房断电、光纤被挖断等场景出现服务不可用的场景,实现服务高可用,同时这多个数据中心之间需要进行数据相互同步来保证数据的最终一致性。

    二. 哪些业务需要异地多活,哪些可以做到异地多活

    通过异地多活实现高可用并不是在所有场景下都可以做到的,通常需要结合业务重要程度,以及业务是否存在全局逻辑,以及异地多活场景中解决全局逻辑的成本来考虑是否需要做异地多活,同时需要考虑业务是否需要保证数据强一致性,由CAP理论可知,CA是不能同时实现的。

    如针对用户中心的场景,注册和登录是两大核心业务,其中对于注册,需要保证唯一性,不能出现用户可以同时注册多个账号的场景;对于登录需要保证不会出现不能登录的场景;

    针对以上两个业务,注册需要保证用户只能注册一个账号,故存在全局约束,即需要保证数据在各个数据中心实时一致性,不能用户在一个数据中心注册之后,这个数据中心在还没同步数据到其他数据中心之前挂了,出现用户在另外一个数据中心又注册一次的场景,所以这种场景如果需要实现异地多活,则需要使用分布式全局唯一ID来实现(如保证一个电话号码只能对应一个,同时需要记录是否使用过,此处还会涉及注册与这个关系的维护的分布式事务问题;或者使用超时时间,该ID生成一段时间之内不能重复申请,但是这段时间内如果刚开始发起申请的机房挂了,则用户不能马上在另外一个机房继续注册),同时又会涉及到这些基础服务的可用性问题,所以是不适合做异地多活的,并且在实际场景中这些业务也是能够容忍短暂不可用场景的。

    而登录是用户中心最重要的业务场景,如果用户登录不了了,特别是重度用户,则是非常严重的事故,所以登录是需要做异地多活的。同时每个数据中心都有用户的账号和密码信息,故可以做到异地多活。

    所以适合做异地多活的业务主要是针对没有全局约束的场景,如只读业务或者无状态服务,典型的如API网关,是非常适合和容易做异地多活的,而像银行实时转账这种场景是不能做异地多活的,如用户在机房A取款了1万块钱,机房A挂了没有同步数据给机房B,此时用户又可以马上去机房B取款1万块钱(即此时用户在ATM前的取款请求发送给机房B),从而导致资损问题,这种是典型的需要保证全局数据实时一致性的场景。由CAP理论可知,C和A是不能同时达到的,故需要牺牲一定的可用性,即机房A挂了之后,用户就不能再执行取款操作了。

    三. 怎么做异地多活

    异地多活的实现难点主要是如何缩短不同数据中心之间的数据同步的网络延迟时间,使不同数据中心的数据尽快达到一致性。由于网络传输固有的时间延迟(延迟一般使用RTT来衡量,RTT是指从发送端发送数据开始到发送端接收到接收端响应的ACK的这段时间的长度,其中主要包括链路的传输时间,系统端的处理时间以及路由器的排队和缓存时间的叠加),以及可能会出现的网络抖动,所以我们只能尽量缩短数据同步的时间。

    设计要点

    所以主要的解决方法以下几种:

    1. 传输速度加快:尽量减少异地多活机房的距离,同时搭建高速网络;
    2. 传输数据减少:尽量减少数据同步,只传输必须的数据;
    3. 保证最终一致性,不保证实时一致性,同时需要增加兜底机制,如数据延迟情况下,二次读取,即当前机房没有数据,去其他机房再查下一次。

    数据同步方式

    对于数据同步方式,一般包括以下几种:

    1. 数据同步本身
      (1)存储系统本身的同步机制,如MySQL的binlog,Redis的主从同步等;
      (2)借助消息中间件来进行数据同步;
    2. 数据同步延迟或非重要数据不同步,补偿机制
      (1)二次读取:数据同步延迟,根据路由规则到产生该数据的机房读取;
      (2)回源读取:非重要、不需要同步的数据,根据路由规则去产生该数据的机房读取;
      (3)重新生成数据:如登录的一个机房挂了,到另外一个机房时,用户重新登录,生成session数据数据。

    四. 总结

    最后一句:没有100%完美的架构,如CAP所阐述的,不能同时达到服务100%可用,又要达到数据100%的实时一致性。同时技术架构是为业务服务的,如果技术架构无法解决业务需要达到的场景,则需要从业务上转变思路,改善业务流程。

    参考
    https://blog.csdn.net/yah99_wolf/article/details/52083089

    展开全文
  • 浅谈Java线程机制

    千次阅读 多人点赞 2014-06-30 20:39:31
    浅谈Java线程机制 (文中重点信息将用红色字体凸显)

    浅谈Java多线程机制

    (-----文中重点信息将用红色字体凸显-----)


    一、话题导入

           在开始简述Java多线程机制之前,我不得不吐槽一下我国糟糕的IT界技术分享氛围和不给力的互联网技术解答深度。当一个初学java的小哥向我请教Java多线程机制相关问题时,我让他去寻求度娘的帮助,让他先学会自己尝试解决问题;但是他告诉我在网上找不到他想要的信息,我也尝试性的在网上收刮了半天,也确实找不到内容详尽、表述清晰的文献。更遗憾的是某些也许有一定参考价值的文档都需要通过非正常手段下载,比如注册、回复甚至是花钱购买,这难免会让不少人望而却步,最后不了了之。

           我并不是蓄意抨击,而是希望更多的人能够向LINUX自由之父Stallman一样,学会奉献;如果大家都能够尝试去奉献,最终每个人也将更易于索取。

           (以后得空将会陆续将Java各知识点归类总结,并放在CSDN个人博客中;出Java之外还考虑介绍下其他方面的内容,届时请保持关注哟^(  。。)^


    二、现实中的类似问题

           假设你是某快餐店的老板,随着自己的苦心经营,终于让快餐店门庭若市、生意兴隆;为了拓展销路,你决定增加送餐上门服务,公司财务告诉你你可以为拓展此业务支配12万元,这个时候你会怎么支配这笔钱呢?

           当然有很多种支配方式,并且在支配上需要考虑到人员数量、送餐范围、送餐形式等多个问题;这里我们集中讨论下送餐形式这个细节:

           1)买一辆雪弗兰赛欧;

           2)买15辆电瓶车;

           除去员工工资等基本成本过后剩余的钱用于购买送餐工具,上面我给出了两种送餐交通工具,他们都有各自的优点:首先,雪弗兰赛欧能够达到更快的送餐速度,而且可以供应的送餐范围更广;其次,用电瓶车作为送餐交通工具可以同时为多个顾客派送,并且运送成本显然更加低廉。在这两者之间,你会作何选择呢?

           显然是第二种送餐交通工具更加实用:相较之下,后者可以处理的顾客数量更多,靠后的顾客等待时间明显缩短。试想一下,如果你打了电话定了午饭,就因为你是第25个顾客,晚上六点才给你送来,你会是什么心情?

           其实,快餐店老板选择多辆电瓶车进行送餐的考虑同进程选择多线程控制的思想是如出一辙的,单线程的程序往往功能非常有限,在某些特定领域甚至不能达到我们所期望的效能。例如,当你想让服务器数据能够被多个客户同时访问时,单线程将让这一设想化为泡影;单线程情况下,多个客户的需求将存入一个栈队,并且依次执行,靠后的客户很难有较好的访问体验。

           Java语言提供了非常优秀的多线程支持,多线程的程序可以包含多个顺序执行流,且多个顺序执行流之间互不干扰。总的来说,使用多线程编程有如下多个优点:

           1)多个线程之间可以共享内存数据;

           2)多个线程是并发执行的,可以同时完成多个任务;

           3)Java语言内置了多线程功能支持,从而简化了Java的多线程编程。


    三、线程的创建和启动

           Java使用Thread类代表线程,所有线程对象都是Thread类或者其子类的实例。创建线程的方式有三种,分别是:

           1)继承Thread类创建线程;

           2)实现Runnable接口创建线程;

           3)使用Callable和Future创建线程。

           以上三种方式均可以创建线程,不过它们各有优劣,我将在分别叙述完每一种创建线程的方式后总结概括。

           3.1 继承Thread类创建线程

           主要步骤为:

           ① 定义一个类并继承Thread类,此类中需要重写Thread类中的run()方法,这个run()方法就是多线程都需要执行的方法;整个run()方法也叫做线程执行体;

           ② 创建此类的实例(对象),这时就创建了线程对象;

           ③ 调用线程对象的start()方法来启动该线程。

           举例说明:

    <span style="font-size:12px;">
    public class MyThread extends Thread 
    {
    	public static void main(String[] args) 
    	{
    		MyThread m1 = new MyThread();
    		MyThread m2 = new MyThread();
    		m1.start();//调用start()方法来开启线程
    		m2.start();</span>
    	}
    
    	private int a;
    	public void run()//重写run()方法
    	{
    		for ( ; a<100 ; a++ )
    		{
    			System.out.println(getName()+"-----"+a);
    			//通过继承Thread类来创建线程时,可以通过getName()方法来获取线程的名称
    		}
    	}
    }
    </span>
           上面通过一个简单的例子演示了创建线程的第一种方法(通过继承Thread类创建线程);通过运行以上代码发现有两个线程在并发执行,它们各自分别打印出0-99。由于没有对线程进行显示的命名,所以系统默认这两个线程的名称为Thread-0和Thread-1,num会跟随线程的个数依次递增。具体怎样定义线程名称,我将在后面提及。

           那么在上述例子中一共有多少个线程在运行呢?答案是三个!

           分别是main(主线程)、Thread-0和Thread-1;我们在多线程编程时一定不要忘记Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体;同理,run()方法就是新建线程的线程执行体。

          PS: 其实上述例子中创建线程的代码(标红)可以简化,使用匿名对象来创建线程:

    <span style="font-family:Microsoft YaHei;font-size:12px;"><span style="font-size:12px;">new MyThread().start();
    new MyThread().start();
    </span></span>

    ------------------------------------------------------------------------------------------------------------------------------------------      

         程序中如果想要获取当前线程对象可以使用方法:Thread.currentThread();

          如果想要返回线程的名称,则可以使用方法:getName();

          故如果想要获取当前线程的名称可以使用以上二者的搭配形式:Thread.currentThread().getName();

          此外,还可以通过setName(String name)方法为线程设置名字;具体操作步骤是在定义线程后用线程对象调用setName()方法:

    <span style="font-family:Microsoft YaHei;font-size:12px;">MyThread m1 = new MyThread();
    m1.setName("xiancheng1");</span>
         如此便能将线程对象m1的名称由Thread-0改变成xiancheng1。

    ------------------------------------------------------------------------------------------------------------------------------------------

          在讨论完设置线程名称及获取线程名称的话题后,我们来分析下变量的共享。从以上代码运行结果来看,线程Thread0和线程Thread1分别输出0-99,由此可以看出,使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

          3.2 实现Runnable接口创建线程类

          主要步骤为:

           ① 定义一个类并实现Runnable接口,重写该接口的run()方法,run()方法的方法体依旧是该线程的线程执行体;

          ② 创建定义的类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;

          ③ 调用线程的start()方法来启动该线程。

          举例说明:

    public class MyThread implements Runnable
    {
    	public static void main(String[] args) 
    	{
    		MyThread m1 = new MyThread();
    		Thread t1 = new Thread(m1,"线程1");
    		Thread t2 = new Thread(m1,"线程2");
    		t1.start();
    		t2.start();
    	}
    
    	private int i;
    	public void run()
    	{
    		for ( ; i<100 ; i++ )
    		{
    			System.out.println(Thread.currentThread().getName()+"  "+i);
    		}
    	}
    }
    
         运行上面的程序可以看出:两个子线程的i变量是连续的,也就是说采用Runnable接口的方式创建的两个线程可以共享线程类的实例属性,这是因为我们创建的两个线程共用同一个target(m1),所以多个线程可以共享同一个线程类的实例属性。

          通过对以上两种创建新线程的方法进行比较分析,可以知道两种创建并启动多线程方式的区别是:通过继承Thread类创建的对象即是线程对象,而通过实现Runnable接口创建的类对象只能作为线程对象的target。

          3.3 通过Callable和Future创建线程

          Callable接口是在Java5才提出的,它是Runnable接口的增强版;它提供了一个call()方法作为线程执行体,且call()方法比run()方法更为强大,主要体现在:

          ① call()方法可以有返回值;

          ② call()方法可以申明抛出异常。

          Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Futrue接口提供一个FutureTask实现类,此实现类实现了Future接口,并且实现了Runnable接口,可以作为Thread类的target。不过需要提出的是,Callable接口有泛型限制,Callable接口里的泛型形参类型于call()方法返回值类型相同。

          主要步骤为:(创建并启动有返回值的线程

          ① 创建Callable接口的实现类,并实现call()方法作为线程的执行体,且该call()方法有返回值;

          //不再是void

          ② 创建Callable接口实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;

          ③ 使用FutureTask对象作为Thread对象的target创建并启动新线程;

          ④ 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

          举例说明:

    public class MyThread implements Callable<Integer>//泛型
    {
    	public static void main(String[] args)
    	{
    		MyThread m1 = new MyThread();//创建Callable对象
    		//使用FutureTask来包装Callable对象
    		FutureTask<Integer> task = new FutureTask<Integer>(m1);
    		Thread t1 = new Thread(task,"有返回值的线程");
    		t1.start();//启动线程
    		//获取线程返回值
    		try
    		{
    			System.out.println("子线程的返回值:"+task.get());
    		}
    		catch (Exception ex)
    		{
    			ex.printStackTrace();
    		}
    	}
    	public Integer call()//返回值类型为Integer
    			     //泛型在集合框架部分会详细介绍
    	{
    		int i = 0;
    		for ( ; i<100 ; i++ )
    		{
    			System.out.println(Thread.currentThread().getName()+"  "+i);
    		}
    		return i;//call()可以有返回值
    	}
    }

          其实,创建Callable实现类与创建Runnable实现类没有太大区别,只是Callable的call()方法允许声明抛出异常,而且允许带返回值。

          3.4 三种创建线程方法的对比

          由于实现Runnable接口和实现Callable接口创建新线程方法基本一致,这里我们姑且把他们看作是同一类型;这种方式同继承Thread方式相比较,优劣分别为:

          1.采用实现Runnable接口和Callable接口的方式创建多线程

          ① 优点:

                1)实现类只是实现了接口,所以它还可以继承其他类;

                2)多个线程可以共享一个target,所以适合多线程处理同一资源的模式,从而可以将CPU、代码和数据分开,较好的体现了面向对象的思想。

          ② 缺点:

                1)编程比较复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

          2.采用继承Thread类的方式来创建新线程

          ① 优点:

                1)编写简单,如果需要访问当前线程,则只需要使用this即可。

          ② 缺点:

                1)因为线程已经继承了Thread类,所以不能再继承其他类。

          3.总结

          ① 综合分析,我们一般采用实现Runnable接口和实现Callable接口的方式来创建多线程。


    四、线程的生命周期

          4.1 CPU运行机制简介

          一般情况下,计算机在一段时间内同时处理着多个进程,然而多个线程的执行顺序并不是依次进行的,而是“同时”在进行;其实这是我们产生的“错觉”。CPU有自己的工作频率,我们称之为主频;它的意思是CPU单位时间(一般定义为1s)内处理单元运算的次数。一般来说,频率越高,CPU的性能就更加优越。正是因为CPU有着很高的工作频率,才能在不同进程之间进行快速的切换,才会给我们造成一种多个任务在同时进行的假象。可以这么说,计算机在某一时刻只能处理单个进程的某一段运算单元(多核处理器的计算机除外)。

          4.2 线程的状态

          当新线程被创建后,他并不是一建立就进入运行状态,也不是一直在运行;由于CPU工作时是在多个进程间不停的切换运行,所以线程会处于多种运行状态,它们包括:新建、就绪、运行、阻塞和死亡(不同的人可能对线程状态的分类持不同意见,这里我们就不深究了)。

          1. 新建和就绪状态

          当程序使用了new关键字创建了一个线程之后,该线程就处于新建状态。当线程对象调用了start()方法之后,该线程便处于就绪状态,处于这个状态的线程并没有开始运行,而只是表示它可以运行了;不过该线程具体什么时候开始运行,完全取决于JVM里线程调度器的调度,这是具有随机性的,这是一种抢占式的调度策略。

          需要注意的是:我们启动一个线程使用的是start()方法,而不是调用线程对象的run()方法。调用start()方法来启动线程,系统会把该run()方法当作线程执行体来处理,但如果直接调用线程对象的run()方法,则run()方法会直接被执行,并且在run()方法返回之前其他线程无法并发执行;此时系统会把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

          2. 运行和阻塞状态

          如果处于就绪状态的线程获得了CPU执行权,开始执行run()方法的线程执行体,则该线程便处于运行状态。

          阻塞状态只能由运行状态进入,而处于阻塞状态的线程只有重新回到就绪状态才能开始下一次运行;换句话说:进入阻塞状态的线程不能直接再运行。当然,运行状态的线程并不是只能通过“运行—》阻塞—》就绪—》运行”方法才能重新运行,它可以直接从运行状态恢复到就绪状态,这里要用到yield()方法。

          线程进入阻塞状态的情况有:

          ① 线程调用sleep()方法,主动放弃了可执行资格;

          ② 当前线程想获取的锁被另一个线程所持有;

          ③ 线程在等待被唤醒;

          ④ 线程调用了suspend()方法将该线程挂起。(此方法容易产生死锁,不推荐使用)

          线程从阻塞状态进入就绪状态的情况有:

          ① 线程sleep()时间已到;

          ② 线程成功获取了锁;

          ③ 线程被唤醒;

          ④ 处于挂起状态的线程被调用了resume()恢复方法。

          可以看出线程进入阻塞状态和线程进入就绪状态的方法或途径是一一对应的。

          3. 线程死亡状态

          线程死亡的情况有:

          ① run()或call()方法执行完成,线程正常结束;

          ② 线程抛出异常或者错误;

          ③ 调用线程的stop()方法结束线程。(此方法容易导致死锁,故不推荐使用)

          主线程和其他线程之间的关系:

          一旦我们建立新线程,它将独立于主线程运行,不受主线程的约束和影响,他们拥有相同的地位;当主线程结束时,其他线程不会受其影响而结束。后面会介绍另外一种线程—后台线程,只要前台线程全部结束,后台线程也会自动结束;后台线程充当的是辅助前台线程的角色,所以后台线程也叫“守护线程”。

         为了测试某线程是否已经死亡,可以调用其isAlive()方法,当线程处于就绪、运行和阻塞三种状态时,方法返回值为true,当线程处于其他两种状态时,此方法返回值为false。

         需要注意的是:

          ① 不要试图对一个已经死亡的线程调用start()方法,否则会抛出“IllegalThreadStateException”异常;

          ② 不要试图对一个线程进行多次start()方法调用,否则也会抛出“IllegalThreadStateException”异常

          4. 线程状态转换关系

          关于线程多个状态之间的转换关系,可以用以下转换图来表示:线程状态转换关系

    五、控制线程

          5.1 join线程

          Thread提供线程“插队”的方法,就是让一个线程等待另一个线程完成的方法-----join()方法,目的是让当下线程等待插入线程运行完成后再继续运行。它一般用于将大问题划分成许多小问题,每个小问题用一个小线程完成;这有点像“坐公交车”,公共汽车就是主线线程,乘客就是插入辅助小线程,小线程在“某站”上车,待到目的地就下车,许多小线程为完成自己的目的在特定时间插入又在特定时间结束。

          举例说明:

    public class JoinThreadTest extends Thread//也可通过实现接口来定义
    {
    	public JoinThreadTest(String name)
    	{
    		super(name);
    	}
    	public void run()
    	{
    		int i=0;
    		for ( ; i<200 ; i++ )
    		{
    			System.out.println(getName()+"--第--"+i+"--次");
    			//由于这里是继承Thread类,所以可以直接使用getName()方法来获取线程名称
    		}
    	}
    	public static void main(String[] args) throws Exception
    	{
    		for ( int a = 0 ; a<200 ; a++ )
    		{
    			if (a==20)
    			{
    				JoinThreadTest jt1 = new JoinThreadTest("插入线程");
    				jt1.start();//启动子线程
    				jt1.join();
    			}
    			System.out.println(Thread.currentThread().getName()+"--第--"+a+"--次");
    		}
    	}
    }
         上例中共有两个线程存在,分别是主线程和新建线程jt1,由于虚拟机首先从main()主函数开始读取,所以主函数开始执行,等到变量a等于20时,开始执行if内部代码块,此段代码新建一个线程jt1并启动该线程。由于jt1线程使用了join()方法,则主线程会等待jt1线程执行完成后才能继续执行。需要指出的是,通过继承Thread()类建立新线程,获取线程名称可以直接使用getName()方法,但是由于此方法是非静态方法,所以在主函数执行体中获取主线程名称不能直接使用getName()方法,必须使用完整的获取线程名称的方法-----Thread.currentThread().getName(),否则会报错。

          join()方法有一定的灵活性。由于它的“强制性”,我们在调用此方法后插入线程需要执行完成后原线程才能继续执行,但是有的时候我们并不需要这样的效果,我们可能希望设定插入线程执行一定的时间然后返回原线程继续执行。join()方法的重载形式便应运而生了:

          ① join():等待被插入的线程执行完成;

          ② join(long millis):等待被插入的线程的时间最长为millis毫秒。

          对于第二种方法,会出现两种情况:如果在特定时间内插入线程提前完成,则原线程还是需要等待直到特定时间后才能继续执行;第二种情况是如果插入线程在特定的时间内没有完成执行任务,则原线程不再等待并开始继续执行,如此原线程和插入线程又处于并列运行状态。

          还有另外一种重载形式,但是对时间精度要求过高,几乎没有“用武之地”,这里就不细说了。

          5.2 后台线程

          顾名思义,后台线程就是在后台运行的线程,它是为其他线程提供服务的,所以后台线程也叫做“守护线程”。Java的垃圾回收线程就是典型的后台线程。

          后台线程较为特殊,如果所有的前台线程都死亡,则后台线程也会随之自动死亡;就如无本之木,没有了实际的意义和存在的必要。

          调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。

          举例说明:

    public class DaemonThreadTest extends Thread
    {
    	static int i=0;
    	public void run()
    	{
    		for ( ; i<100 ; i++ )
    		{
    			System.out.println(getName()+"----"+i);
    		}
    	}
    	public static void main(String[] args) 
    	{
    		DaemonThreadTest dtt = new DaemonThreadTest();
    		//设置此线程为后台线程
    		dtt.setDaemon(true);
    		dtt.start();
    		int a=0;
    		while (a<10)
    		{
    			System.out.println(Thread.currentThread().getName()+"~~~~"+a);
    			a++;
    		}
    		if (i<99)
    		{
    			System.out.println("后台线程dtt没有执行完成就退出了!"+i);
    		}
    	}
    }
            笔者在运行以上代码的时候会出现想要的结果:主线程main执行完成后,新建线程Thread-0还没有执行完,最后if语句中的文字”后台线程dtt没有执行完就退出了!“输出;可以证明辅助线程在主线程执行完成后就随之死亡,哪怕自己还没有执行完成。但是笔者在运行上述代码的时候发生了一件看似诧异的事情:if语句输出的i的值比run()方法里的i值要小一点,这其实是容易理解的----前台线程死亡后,JVM会通知后台线程死亡,但是从后台线程接收指令并做出反应需要一定的时间,所以导致run()方法在这个时间差里继续运行,才导致了两个i值不同。

          在这里笔者要重申一点:由于上述代码中选用的i变量范围较小,故有的时候可能看到的情况是dtt线程执行完成了,这并不是代码错误了,而是由于执行内容少代码瞬间就执行完成了,这是由于处理器性能和随机性决定的。如果我们把变量i的范围调整到1000,出现想要结果的可能性就会大很多。

          Thread类提供了一个判断线程是否为后台线程的方法----isDaemon()方法。

          需要指出的是,前台线程如果创建子线程依旧默认是前台线程;同理,后台线程创建的子线程默认是后台线程。此外,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。

          5.3 线程等待---sleep()

          如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()来实现,sleep()方法主要形式为:

    <span style="font-family:Microsoft YaHei;font-size:12px;"> static void sleep(long millis);</span>
          括号里的参数表示的是线程等待的时间。当线程进入等待状态,在暂停时间范围内线程无法”提前“开始执行,哪怕系统中没有其他线程运行。下面的例子将说明这一点:

    public class SleepTest
    {
    	public static void main(String[] args) throws Exception
    	{
    		for (int a=0 ;a<200 ; a++ )
    		{
    			System.out.println("a的值是: "+a);
    			if (a==50)
    			{
    				Thread.sleep(2000);
    				//括号中参数的单位是毫秒
    			}
    		}
    	}
    }
             运行上面代码我们可以发现,程序中只有一个线程----主线程,当for循环执行到a等于50的时候会停顿两秒然后再接着执行直到进程结束。

          5.4 线程让步----yield()

          其实yield()方法同sleep()方法比较类似,它们的共同点就是放弃当前执行权。但是它们也有明显的区别:sleep()方法是让线程放弃当前执行权并转入阻塞状态,而yield()方法是让当前线程放弃执行权后进入就绪状态;此外,前者是规定了线程等待的具体时间,而后者只是让当前线程暂停一下,让线程调度器重新调度,完全可能发生的情况是:当某个线程调用了yield()方法暂停后,线程调度器又将其调度出来重新执行,而期间没有其他线程插入。

          需要指出的是,某个线程执行了yield()方法后,只有优先级大于或等于当前线程优先级的线程才会获得执行机会,等待会介绍了设置线程优先级后笔者会用例子加以说明。

          总结两方法的异同,sleep()方法和yield()方法区别如下:

          ① sleep()方法暂停当前线程后将执行权让出给其他线程,而yield()方法只会把执行权让给优先级大于或等于自己优先级的线程;

          ② sleep()方法将当前线程转入阻塞状态,而yield()方法则把当前线程转入就绪状态;

          ③ sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时需要对异常进行相应,要么处理要么抛出,而yield()方法没有声明抛出任何异常;

          ④ sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行。

          5.5 改变线程的优先级

          简单的说,线程有一定的优先级,优先级从1到10不等;而主线程和新建线程默认优先级为普通,用数字表示就是优先级为5。优先级越高,或的执行权的可能也就越大。Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级;一般情况下,线程优先级如果用数字表示相差不大的情况下效果不是很明显,而且由于用数字表示优先级移植性不佳,故我们一般只取三种优先级,并赋予特殊的名称:

          ① MAX_PRIORITY:其值是10;

          ② MIN_PRIORTY:其值是1;

          ③ NORM_PRIORITY:其值是5。

          结合yield()方法和线程优先级知识,笔者举例加以巩固:

    public class YieldTest extends Thread
    {
    	public YieldTest(String name)
    	{
    		super(name);
    	}
    	static int a=0;
    	public void run()
    	{
    		for ( ; a<100 ; a++ )
    		{
    			System.out.println(getName()+"----"+a);
    			if (a==50)
    			{
    				Thread.yield();
    			}
    		}
    	}
    	public static void main(String[] args) 
    	{
    		YieldTest yt1 = new YieldTest("高级线程");
    		yt1.setPriority(Thread.MAX_PRIORITY);
    		yt1.start();
    		YieldTest yt2 = new YieldTest("低级线程");
    		yt2.setPriority(Thread.MIN_PRIORITY);
    		yt2.start();
    	}
    }
             上述代码共创建了两个新线程,两个线程共用一个变量a;当运行以上代码时可以清楚的看见,在for循环100次的执行过程中,线程yt1(也就是高级线程)获得执行的次数要多余yt2(也就是低级线程)所执行的次数。此外,由于yield()方法的特殊性,我们几乎感觉不到调用了yield()方法带来的线程切换。


    六、线程同步

          6.1 线程安全问题分析

          使用多线程可以提高进程的执行效率,但是它也伴随着一定的风险;这是由系统的线程调度具有一定的随机性造成的,我们首先通过一个大家耳熟能详的例子来说明多线程引发的同步问题----银行取钱。

          我们按照生活中正常的取、存钱操作编写如下代码:

    public class DrawTest
    {
    	public static void main(String[] args)
    	{
    		//创建账户
    		Account acct=new Account("公共账户",1000);
    		//模拟两个线程对同一个账户取钱
    		new DrawThread("客户甲",acct,800).start();//匿名对象
    		new DrawThread("客户乙",acct,800).start();
    	}
    }
    class Account
    {
    	//建立并封装用户编号和账户余额两个变量
    	private String number;
    	private double balance;
    	//建立构造器进行初始化
    	public Account(String number,double balance)
    	{
    		this.number = number;
    		this.balance = balance;
    	}
    	public void setNumber(String number)
    	{
    		this.number=number;
    	}
    /*
            public void setBalance(double balance)
    	{
    		this.balance=balance;
    	}
    */
            public String getNumber()
    	{
    		return number;
    	}
    	public double getBalance()
    	{
    		return balance;
    	}
    	//为了判断用户是否是同一个用户,我们重写hashCode()和equals()方法来进行判断
    	public int hashCode()
    	{
    		return number.hashCode();
    	}
    	public boolean equals(Object obj)
    	{
    		if (this==obj)
    		{
    			return true;
    		}
    		if (obj!=null&&obj.getClass()==Account.class)
    		{
    			Account target = (Account)obj;
    			return target.getNumber().equals(number);
    		}
    		else 
    			return false;
    	}
    }
    class DrawThread extends Thread
    {
    	private Account account;//模拟用户账户
    	private double drawAmount;//希望取钱的数目
    	public DrawThread(String name,Account account,double drawAmount)
    	{
    		super(name);
    		this.account=account;
    		this.drawAmount=drawAmount;
    	}
    	//当多个线程操作同一个数据时,将涉及数据安全问题
    	public void run()
    	{
    		if (account.getBalance()>=drawAmount)//判断余额是否大于取钱数
    		{
    			System.out.println(getName()+"取钱成功!"+drawAmount);
    			try
    			{
    				Thread.sleep(10);
    			}
    			catch (InterruptedException ex)
    			{
    				ex.printStackTrace();//打印异常信息
    			}
    			//修改余额
    			account.setBalance(account.getBalance()-drawAmount);
    			System.out.println("余额为:"+account.getBalance());
    		}
    		else 
    		{
    			System.out.println(getName()+"取钱失败,余额不足!");
    		}
    	}
    }
    
             运行上面代码会发现不符合实际的情况发生:账户余额只有1000却取出了1600,而且账户余额出现了负值,这不是银行希望的结果。这种滑稽的错误是因为线程调度的不确定性,run()方法的方法体不具有同步安全性;程序中有两个并发线程在修改Account对象。

          6.2 同步代码块

          由银行取钱“风波”可以了解到,当有两个进程并发修改同一个文件时就有可能造成异常。为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:

    <span style="font-family:Microsoft YaHei;font-size:12px;">       synchronized (对象)
    	   {
    		   需要被同步的代码;
    	   }
    </span>
             上面代码中,synchronized后括号中的对象就是同步监视器,线程在执行同步代码块之前需要先获得同步监视器的锁。同步代码块的同步监视器为对象,我们一般选用Object类来创建对象,这个对象就是锁。
    <span style="font-family:Microsoft YaHei;font-size:12px;">       Object obj = new Object();</span>
             任何时刻只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放锁。如此,我们将银行取钱问题的代码稍加修改,就能达到我们想要的运算结果:

    	public void run()
    	{
    		synchronized(account)
    		{
    			if (account.getBalance()>=drawAmount)//判断余额是否大于取钱数
    			{
    				System.out.println(getName()+"取钱成功!"+drawAmount);
    				try
    				{
    					Thread.sleep(10);
    				}
    				catch (InterruptedException ex)
    				{
    					ex.printStackTrace();//打印异常信息
    				}
    				//修改余额
    				account.setBalance(account.getBalance()-drawAmount);
    				System.out.println("余额为:"+account.getBalance());
    			}
    			else 
    			{
    				System.out.println(getName()+"取钱失败,余额不足!");
    			}
    		}
    	}
    
            上面程序使用synchronized将run()方法里的方法体修改为同步代码块,该同步代码块的同步监视器就是account对象,这样的做法符合“加锁-修改-解锁”的逻辑;通过这种方式可以保证并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区,从而保证了线程的安全性。

          6.3 同步函数

          同步函数就是使用synchronized关键字修饰的方法,同步函数的同步监视器是this,也就是调用方法的对象本身。需要指出的是,synchronized关键字可以修饰方法和代码块,但是不能修饰构造器、属性等。

          同步的前提:

          ① 必须要有两个或两个以上的线程;

          ② 必须是多个线程使用同一个锁;

          ③ 必须保证同步中只有一个线程在运行;

          为了减少保证线程安全而带来的负面影响(例如更加消耗资源),程序可以进行优化和控制:

          ① 只对那些会改变竞争资源的方法或代码进行同步;

          ② 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本,即线程安全版本和线程不安全版本。

          如果同步函数为静态同步,则其同步监视器就是:类名.class。

          6.4 同步在单例设计模式中的应用

          单例设计模式,顾名思义就是一个类只能创建一个对象;单例设计模式一共分为两种,分别是饿汉式和懒汉式。由于饿汉式在一开始就建立了对象并初始化提供了调用的方法,故饿汉式在多线程情况下没有安全隐患,不会引起多线程异常;而懒汉式由于需要对对象是否为空进行判断,所以可能导致多线程异常。

          饿汉式单例设计模式:

    	   class single
    	   {
    		   private static single s = new single;
    		   private single(){}
    		   public static single getInstance()
    		   {
    				return s;
    		   }
    	   }
    
            懒汉式单例设计模式:
    	   class single
    	   {
    		   private static single s = null;
    		   private single(){}
    		   public static single getInstance{}
    		   {
    				if (s==null)
    				{
    					synchronized(single.class)
    					{
    						if (s==null)//二次判断
    						{
    							s=new single();
    						}
    					}
    				}
    				return s;
    		   }
    	   }
    

          6.5 释放同步监视器的锁定

          线程会在如下几种情况释放对同步监视器的锁定:

          ① 当前线程的同步方法或者同步代码块执行完毕;

          ② 当前线程的同步方法或者同步代码块中遇到break、return终止了该代码块或该方法的继续执行;

          ③ 当前线程的同步方法或者同步代码块中遇到未处理的error或Exception,导致了该代码块或该方法异常而结束;

          ④ 程序执行了同步监视器对象的wait()方法。

          线程在如下情况下不会释放同步监视器:

          ① 程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行;

          ② 其他线程调用了当前线程的suspend()方法将当前线程挂起。

          6.6 同步锁

          从Java5开始,Java提供了一种功能更加强大的线程同步机制-----通过显示定义同步锁对象来实现同步,在这种机制下,同步锁采用Lock对象充当。

          Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现了允许更灵活的结构,Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显示的加锁、释放锁。

          举例说明:

    class LockTest 
    {
    	//用ReentrantLock类定义锁对象
    	private final ReentrantLock lock= new ReentrantLock();
    	//将此锁应用在需要保证线程安全的方法上
    	public void test()
    	{
    		//加锁
    		lock.lock();
    		try
    		{
    			//需要保证线程安全的代码
    		}
    		catch (Exception e)
    		{
    			System.out.println("发生错误信息,请重新确认代码!");
    		}
    		finally
    		{
    			//释放锁
    			lock.unlock();
    		}
    	}
    }
    
            使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。前面介绍的银行存取钱例子中,可以使用ReentrantLock类定义的锁来保证线程安全,而且相较于synchronized代码块或synchronized方法更加简洁方便。

          使用Lock与使用同步方法有点类似,只是使用Lock时显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。此外,Lock提供了同步方法和同步代码块所没有的其他功能:

          ① 用于非结构块的tryLock()方法;

          ② 试图获取可中断锁的lockInterruptibly()方法;

          ③ 获取超时失效锁的tryLock(long,TimeUnit)方法。

          ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套使用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

          6.7 死锁

          当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应采取措施避免死锁出现;一旦出现死锁,整个程序既不会发生任何异常,也不会给出提示,只是所有线程处于阻塞状态,无法继续。

          举例说明:

    class  A
    {
    	public synchronized void foo(B b)
    	{
    		System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"进入了A实例的foo方法");
    		try
    		{
    			Thread.sleep(200);
    		}
    		catch (InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"企图调用B实例的last方法");
    		b.last();
    	}
    	public synchronized void last()
    	{
    		System.out.println("进入了A类的last方法内部");
    	}
    }
    class B
    {
    	public synchronized void bar(A a)
    	{
    		System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"进入了B实例的bar方法");
    		try
    		{
    			Thread.sleep(200);
    		}
    		catch (InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"企图调用A实例的last方法");
    		a.last();
    	}
    	public synchronized void last()
    	{
    		System.out.println("进入了B类的last方法内部");
    	}
    }
    public class DeadLockTest implements Runnable
    {
    	A a=new A();
    	B b=new B();
    	public void init()
    	{
    		Thread.currentThread().setName("主线程");
    		a.foo(b);
    		System.out.println("进入了主线程之后");
    	}
    	public void run()
    	{
    		Thread.currentThread().setName("副线程");
    		b.bar(a);
    		System.out.println("进入了副线程之后");
    	}
    	public static void main(String[] args)
    	{
    		DeadLockTest d1=new DeadLockTest();
    		new Thread(d1).start();
    		d1.init();
    	}
    }
    

          6.8 线程通信

          线程间通信方法:

          ① wait():导致当前线程等待,括号中可以定义等待时间,若不定义等待时间,则需要等待至被唤醒;

          ② notify():唤醒在此同步监视器上等待的单个线程,如果多个线程在等待,则随机唤醒其中一个线程;

          ③ notifyAll():唤醒在此同步监视器上的所有线程。

          需要注意的是,以上三个方法并不属于Thread类,而是属于Object类。对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法;而同步代码块中同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。

          如果程序不是通过synchronized关键字来保证同步,而是使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用上述三个方法来进行线程间通信了,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。Condition实例被绑定在一个Lock对象上,要活的特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。同样的,Condition类提供了如下3个方法:

          ① await():类似于wait()方法,导致当前线程等待;

          ② signal():唤醒在此Lock对象上等待的单个线程;

          ③ signalAll():唤醒在此Lock对象上等待的所有线程。   

          这里还是以取钱的例子来说明:

    public class Account
    {
    	private final Lock lock=new ReentrantLock();
    	private final Condition cond=lock.newCondition();
    	private String accountNo;
    	private double balance;
    	private boolean flag=false;
    	public Account(){}
    	public Account(String accountNo,double balance)
    	{
    		this.accountNo=accountNo;
    		this.balance=balance;
    	}
    	public void setAccountNo(String accountNo)
    	{
    		this.accountNo=accountNo;
    	}
    	public String getAccountNo(String accountNo)
    	{
    		return accountNo;
    	}
    	public double getBalance(double balance)
    	{
    		return balance;
    	}
    	public void draw(double drawAmount)
    	{
    		lock.lock();
    		try
    		{
    			if (!flag)
    			{
    				cond.await();
    			}
    			else
    			{
    				System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount);
    				balance -= drawAmount;
    				System.out.println("账户余额为:"+balance);
    				flag=false;
    				cond.signalAll();
    			}
    		}
    		catch (InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		finally
    		{
    			lock.unlock();
    		}
    	}
    	public void deposit(double depositAmount)
    	{
    		lock.lock();
    		try
    		{
    			if (flag)
    			{
    				cond.await();
    			}
    			else
    			{
    				System.out.println(Thread.currentThread().getName()+"存款:"+depositAmount);
    				balance+=depositAmount;
    				System.out.println("账户余额为:"+balance);
    				flag=true;
    				cond.signalAll();
    			}
    		}
    		catch (InterruptedException e )
    		{
    			e.printStackTrace();
    		}
    		finally
    		{
    			lock.unlock();
    		}
    	}
    }

    七、其他内容

          以上是关于多线程机制的基础内容,除此之外,还有关于"线程组和未处理的异常"、"线程池"等基础概念及内容,在这里笔者就不详细阐述了,读者打好多线程机制的基础后,可以自行学习这些拓展内容。

          多线程处理机制,就介绍到这里啦!


    展开全文
  • java线程并发机制

    千次阅读 2017-04-27 10:59:33
    一、线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • 异地多活没那么难

    万次阅读 2018-03-09 17:46:28
    1. 引言有幸参与了阿里游戏的一个高可用方案的设计,并且在网上发表了方案(面向业务的立体化高可用架构设计),后来参加GOPS全球运维大会深圳站,与众多行业高手交流,发现大家对“异地多活”这个方案设计非常感...
  • 异地多活的单元化设计

    千次阅读 2018-05-15 14:04:36
    原创声明:本文系作者原创,谢绝个人、媒体、公众号或...单元技术是阿里巴巴异地多活实践中总结出来的基于核心业务的跨机房分流量技术,如之前提到,实际的软件系统技术上很难100%业务使用多活,只能尽量保证绝大部...
  • 异地多活问题

    千次阅读 2018-05-30 20:14:25
    1. 引言有幸参与了阿里游戏的一个高可用方案的设计,并且在网上发表了方案(面向业务的立体化高可用架构设计),后来参加GOPS全球运维大会深圳站,与众多行业高手交流,发现大家对“异地多活”这个方案设计非常感...
  • 关于存储控制器的路径机制

    万次阅读 2012-05-02 11:19:30
    关于存储控制器的路径机制   目前,MS3000/MS5000控制器支持ALUA路径机制(或者说负载均衡技术),什么是ALUA路径机制? ALUA即“Asymmetric Logical Unit Access(异步逻辑单元访问)”的缩写,它是前端...
  • 陈永庭,饿了么框架工具部高级架构师,主要负责MySQL异地双向数据复制,支撑饿了么异地多活项目。曾就职于WebEx、Cisco、腾讯等公司。 今天我主要分享饿了么多活的底层数据实施,会和大家介绍在整个多活的设计和...
  • 饿了么异地多活技术实现

    千次阅读 2018-04-02 00:00:00
    饿了么技术团队花了1年的时间,实现了业务的整体异地多活,能够灵活的在个异地机房之间调度用户,实现了自由扩容和机房容灾的目标。本文介绍这个项目的整体结构,还简要介绍实现多活的5大核心基础组件,为读者...
  • RabbitMQ集群架构模式-多活模式(Federation)--异地数据复制的主流模式 RabbitMQ集群架构模式-镜像模式(Mirror)--常用 RabbitMQ集群架构模式-主备模式(Warren)--并发和数据量不高 Rab...
  • JVM类加载机制   .java文件编译—&gt;生成JVM能够识别的.class字节码文件—&gt;JVM把.class文件加载到内存—&gt;对数据进行校验、转换解析、初始化 后续由执行引擎执行,在执行过程中,需要运行时...
  • 虽然Struts的Form验证机制给开发提高了效率,但是,在实际开发中,书写大量定义验证规则的XML仍旧是一项非常繁琐的工作。因为,XML定义文件是由人来编写的,一旦出现输入错误,将给挑错带来很大的难度。(这也是...
  • 异地多活架构设计

    千次阅读 2016-07-21 17:28:23
    有幸参与了阿里游戏的一个高可用方案的设计,并且在网上发表了方案(面向业务的立体化高可用架构设计),后来参加GOPS全球运维大会深圳站,与众多行业高手交流,发现大家对“异地多活”这个方案设计非常感兴趣,毕竟...
  • 要做到全球异地多活, 一定要在数据层支持机房写入, 并且对大多数业务场景提供最终一致性的解决方案。原因如下: 跨洲的网络延迟在100ms的数量级,如果只有单点写, 对于用户体验是种灾难 对于高频操作来说, ...
  • 先前未做双主双集群; 一台数据库节点有历史数据; 现在要做双主双Galera集群. 场景模拟演练 创建一台有历史数据的数据库节点 从另一个MySQL数据库中,备份所有数据,然后将数据导入到本地数据库节点. 安装同...
  • 一文揭秘阿里云Redis全球多活产品

    千次阅读 2018-08-30 14:39:42
    摘要: Redis全球多活产品是阿里云自研、基于云数据库Redis版(ApsaraDB for Redis)、100%兼容 Redis 协议的多活数据库系统。通过数据同步通道,把个Redis实例组网成1个逻辑上的 Redis 多活实例,多活实例内的所有...
  • 互联网异地多活方案发展历史

    千次阅读 2018-05-12 14:52:35
    异地多活技术是随着互联网环境进步发展到PB量级后,出现并逐步完善的一种用于解决高并发、大流量、高可用、热灾备需求的分布式解决方案。近4、5年,随着互联网环境的不断进步,当前异地多活技术是仅次于微服务架构、...
  • 有幸参与了阿里游戏的一个高可用方案的设计,并且在网上发表了方案(面向业务的立体化高可用架构设计),后来参加GOPS全球运维大会深圳站,与众多行业高手交流,发现大家对“异地多活”这个方案设计非常感兴趣,毕竟...
  • 巨杉数据库多活架构实践

    千次阅读 2018-08-17 16:24:56
    巨杉数据库助力民生银行、广发银行前台智慧化业务   今年以来,公有云事故频发,大有“黑天鹅”不断爆发之势头。...通过双多活以及高可用灾备等机制不断创新,数据库安全将会提升一个新的台阶。
  • Redis的哨兵机制 或者心跳机制 模式 原理详解

    万次阅读 多人点赞 2018-08-20 23:02:09
    转载自https://blog.csdn.net/yswKnight/article/details/78158540 ...amp;fps=1 一.什么是哨兵机制? 答:Redis的哨兵(sentinel) 系统用于管理个 Redis 服务器,该系...
  • 哨兵 (sentinal) 机制的工作原理

    千次阅读 2019-10-16 17:47:49
    Redis的哨兵(sentinel) 系统用于管理个 Redis 服务器,该系统执行以下三个任务: 监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。 提醒(Notification):当被监控的某个 Redis出现...
  • IOS 线程 RUNLOOP 机制 (二)

    千次阅读 2013-02-22 10:56:49
    对于辅助线程,在需要和线程有更交互时,才使用Run Loop。 比如:1)使用端口或者自定义输入源来和其他线程通讯 2)使用线程定时器 3)Cocoa中使用任何performSelector...的方法(参考Table:Performing ...
  • 了解并发的底层原理有助于从更高层次认知线程的工作原理,从应用角度讲,有助于我们构建高效健壮的并发应用和解决实际的生产问题。并发的实现并不是仅仅由JVM实现,而是JVM联合处理器指令共同完成,本节就volatile...
  • 传送机制以及ACK机制详解

    千次阅读 2017-12-05 16:39:16
    转载:http://shift-alt-ctrl.iteye.com/blog/2020182AcitveMQ是作为一种消息存储和分发组件,涉及到client与... ActiveMQ消息传送机制 Producer客户端使用来发送消息的, Consumer客户端用来消费消息;它们的协...
  • kafka容灾机制

    千次阅读 2017-03-09 19:40:06
    Kafka的消息安全性与容灾机制主要是通过副本replication的设置和leader/follower的的机制实现的。 Replication机制broker 容灾机制 Leader Election机制 controller failover Replication...
  • 最近在看一本书《蚂蚁金服-科技金融独角兽的崛起》,正看到第七章“技术攻坚”简单介绍了一下支付宝的架构从第一代的烟囱架构、第二代的分布式架构,第三代的云计算架构,其中有个词“异地多活”出现了一次,但没有...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 75,981
精华内容 30,392
关键字:

多活机制