alertmanager集群莫名发送resolve消息的问题探究

术语

  • 告警消息:指一条告警
  • 告警恢复消息:指一条告警恢复
  • 告警信息:指告警相关的内容,包括告警消息和告警恢复消息

问题描述

最近遇到了一个alertmanager HA集群莫名发送告警恢复消息的问题。简单来说就是线上配置了一个一直会产生告警的规则,但却会收到alertmanager发来的告警恢复消息,与预期不符。

所使用的告警架构如下,vmalert产生的告警会通过LB发送到某个后端alertmanager实例。原本以为,接收到该告警的alertmanager会将告警信息同步到其他实例,当vmalert产生下一个相同的告警后,则alertmanager实例中的第二个告警会刷新第一个告警,后续通过告警同步将最新的告警发送到各个alertmanager实例,从而达到抑制告警和抑制告警恢复的效果(。

但在实际中发现,alertmanager对一直产生的告警发出了告警恢复消息。

问题解决

问题解决办法很简单:让告警直接发送到alertmanager HA集群的每个实例即可。

Question regarding Loadbalanced Alertmanager ClustersAlerting issues with Alertmanage这两篇文档中描述了使用LB导致alertmanager HA集群发生告警混乱的问题。此外在官方文档也有如下提示:

It's important not to load balance traffic between Prometheus and its Alertmanagers, but instead, point Prometheus to a list of all Alertmanagers.

但根因是什么,网上找了很久没有找到原因。上述文档描述也摸棱两可。

问题根因

首先上一张alertmanager官方架构图

注意到上图有三类provider:

  1. Alert Provider:负责处理通过API传入的告警,vmalert(Prometheus)产生的告警就是在这里接收处理的
  2. Silence Provider:负责处理静默规则,本次不涉及告警静默,因此不作讨论。
  3. Notify Provider:负责实例间发送告警信息。

根据如上分析,可以得出,一个alertmanager实例有两种途径获得告警信息,一种是由外部服务(如vmalert、Prometheus等)通过API传入的,另一种是通过alertmanager 实例间的Notification Logs消息获得的。

注:需要说明的是,alertmanager判定一个告警是不是恢复状态,主要是通过该告警的EndsAt字段,如果EndsAt时间点早于当前时间,说明该告警已经失效,需要发送告警恢复,判断代码如下:

// Resolved returns true iff the activity interval ended in the past.
func (a *Alert) Resolved() bool {
return a.ResolvedAt(time.Now())
} // ResolvedAt returns true off the activity interval ended before
// the given timestamp.
func (a *Alert) ResolvedAt(ts time.Time) bool {
if a.EndsAt.IsZero() {
return false
}
return !a.EndsAt.After(ts)
}

API Provider的处理

alertmanager提供了两套API:v1和v2。但两个API内部处理还是一样的逻辑,以v1 API为例,

入口函数为insertAlerts,该函数主要负责告警的有效性校验,处理告警的StartAtEndAt,最后通过Put方法将告警保存起来。

本案例场景中,vmalert会给所有告警加上EndAt,值为:当前时间 + 4倍的groupInterval(默认1min) = 4min。

func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) {
now := time.Now() api.mtx.RLock()
resolveTimeout := time.Duration(api.config.Global.ResolveTimeout)
api.mtx.RUnlock() for _, alert := range alerts {
alert.UpdatedAt = now // Ensure StartsAt is set.
if alert.StartsAt.IsZero() {
if alert.EndsAt.IsZero() {
alert.StartsAt = now
} else {
alert.StartsAt = alert.EndsAt
}
}
// If no end time is defined, set a timeout after which an alert
// is marked resolved if it is not updated.
if alert.EndsAt.IsZero() {
alert.Timeout = true
alert.EndsAt = now.Add(resolveTimeout)
}
if alert.EndsAt.After(time.Now()) {
api.m.Firing().Inc()
} else {
api.m.Resolved().Inc()
}
} // Make a best effort to insert all alerts that are valid.
var (
validAlerts = make([]*types.Alert, 0, len(alerts))
validationErrs = &types.MultiError{}
)
for _, a := range alerts {
removeEmptyLabels(a.Labels) if err := a.Validate(); err != nil {
validationErrs.Add(err)
api.m.Invalid().Inc()
continue
}
validAlerts = append(validAlerts, a)
}
if err := api.alerts.Put(validAlerts...); err != nil {
api.respondError(w, apiError{
typ: errorInternal,
err: err,
}, nil)
return
} if validationErrs.Len() > 0 {
api.respondError(w, apiError{
typ: errorBadData,
err: validationErrs,
}, nil)
return
} api.respond(w, nil)
}

Put函数中会对相同指纹的告警进行Merge,这一步会刷新保存的对应告警的StartAtEndAt,通过这种方式可以保证告警的StartAtEndAt可以随最新接收到的告警消息而更新。

func (a *Alerts) Put(alerts ...*types.Alert) error {
for _, alert := range alerts {
fp := alert.Fingerprint() existing := false // Check that there's an alert existing within the store before
// trying to merge.
if old, err := a.alerts.Get(fp); err == nil {
existing = true // Merge alerts if there is an overlap in activity range.
// 更新告警的StartAt和EndAt字段
if (alert.EndsAt.After(old.StartsAt) && alert.EndsAt.Before(old.EndsAt)) ||
(alert.StartsAt.After(old.StartsAt) && alert.StartsAt.Before(old.EndsAt)) {
alert = old.Merge(alert)
}
} if err := a.callback.PreStore(alert, existing); err != nil {
level.Error(a.logger).Log("msg", "pre-store callback returned error on set alert", "err", err)
continue
} if err := a.alerts.Set(alert); err != nil {
level.Error(a.logger).Log("msg", "error on set alert", "err", err)
continue
} a.callback.PostStore(alert, existing) // 将告警分发给订阅者
a.mtx.Lock()
for _, l := range a.listeners {
select {
case l.alerts <- alert:
case <-l.done:
}
}
a.mtx.Unlock()
} return nil
}

根据上述分析可以得出,当通过API获取到相同(指纹)的告警时,会更新本实例对应的告警信息(StartAtEndAt),因此如果通过API不停向一个alertmanager实例发送告警,则该实例并不会产生告警恢复消息。

下一步就是要确定,通过API接收到的告警信息是如何发送给其他实例的,以及发送的是哪些信息。

从官方架构图上可以看出,API接收到的告警会进入Dispatcher,然后进入Notification Pipeline,最后通过Notification Provider将告警信息发送给其他实例。

Dispatcher的处理

在上面Put函数的最后,会将Merge后的告警发送给a.listeners,每个listener对应一个告警订阅者,Dispatcher算是一个告警订阅者。

要获取从API 收到的告警,首先要进行订阅。订阅函数如下,其实就是在listeners新增了一个channel,该channel中会预先填充已有的告警,当通过API接收到新告警后,会使用Put()方法将新的告警分发给各个订阅者。

// Subscribe returns an iterator over active alerts that have not been
// resolved and successfully notified about.
// They are not guaranteed to be in chronological order.
func (a *Alerts) Subscribe() provider.AlertIterator {
a.mtx.Lock()
defer a.mtx.Unlock() var (
done = make(chan struct{})
alerts = a.alerts.List()
ch = make(chan *types.Alert, max(len(alerts), alertChannelLength))
) for _, a := range alerts {
ch <- a
} a.listeners[a.next] = listeningAlerts{alerts: ch, done: done}
a.next++ return provider.NewAlertIterator(ch, done, nil)
}

alertmanager的main()函数中会初始化启动一个Dispatcher

// Run starts dispatching alerts incoming via the updates channel.
func (d *Dispatcher) Run() {
d.done = make(chan struct{}) d.mtx.Lock()
d.aggrGroupsPerRoute = map[*Route]map[model.Fingerprint]*aggrGroup{}
d.aggrGroupsNum = 0
d.metrics.aggrGroups.Set(0)
d.ctx, d.cancel = context.WithCancel(context.Background())
d.mtx.Unlock() d.run(d.alerts.Subscribe())
close(d.done)
}

Dispatcher启动之后会订阅告警消息:

// Run starts dispatching alerts incoming via the updates channel.
func (d *Dispatcher) Run() {
...
// 订阅告警消息
d.run(d.alerts.Subscribe())
close(d.done)
}

下面是Dispatcher的主函数,负责接收订阅的channel中传过来的告警,并根据路由(route)处理告警消息(processAlert)。

func (d *Dispatcher) run(it provider.AlertIterator) {
cleanup := time.NewTicker(30 * time.Second)
defer cleanup.Stop() defer it.Close() for {
select {
// 处理订阅的告警消息
case alert, ok := <-it.Next():
if !ok {
// Iterator exhausted for some reason.
if err := it.Err(); err != nil {
level.Error(d.logger).Log("msg", "Error on alert update", "err", err)
}
return
} level.Debug(d.logger).Log("msg", "Received alert", "alert", alert) // Log errors but keep trying.
if err := it.Err(); err != nil {
level.Error(d.logger).Log("msg", "Error on alert update", "err", err)
continue
} now := time.Now()
for _, r := range d.route.Match(alert.Labels) {
d.processAlert(alert, r)
}
d.metrics.processingDuration.Observe(time.Since(now).Seconds()) case <-cleanup.C:
d.mtx.Lock() for _, groups := range d.aggrGroupsPerRoute {
for _, ag := range groups {
if ag.empty() {
ag.stop()
delete(groups, ag.fingerprint())
d.aggrGroupsNum--
d.metrics.aggrGroups.Dec()
}
}
} d.mtx.Unlock() case <-d.ctx.Done():
return
}
}
}

processAlert主要是做聚合分组的,ag.run函数会填充相关的告警信息,并根据GroupWaitGroupInterval发送本实例非恢复的告警。

从alertmanager的架构图中可以看到,在Dispatcher聚合分组告警之后,会将告警送到Notification Pipeline进行处理,Notification Pipeline的处理对应ag.run的入参回调函数。该回调函数中会调用stage.Exec来处理Notification Pipeline的各个阶段。

// processAlert determines in which aggregation group the alert falls
// and inserts it.
func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) {
groupLabels := getGroupLabels(alert, route) fp := groupLabels.Fingerprint() d.mtx.Lock()
defer d.mtx.Unlock() routeGroups, ok := d.aggrGroupsPerRoute[route]
if !ok {
routeGroups = map[model.Fingerprint]*aggrGroup{}
d.aggrGroupsPerRoute[route] = routeGroups
} ag, ok := routeGroups[fp]
if ok {
ag.insert(alert)
return
} // If the group does not exist, create it. But check the limit first.
if limit := d.limits.MaxNumberOfAggregationGroups(); limit > 0 && d.aggrGroupsNum >= limit {
d.metrics.aggrGroupLimitReached.Inc()
level.Error(d.logger).Log("msg", "Too many aggregation groups, cannot create new group for alert", "groups", d.aggrGroupsNum, "limit", limit, "alert", alert.Name())
return
} ag = newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.logger)
routeGroups[fp] = ag
d.aggrGroupsNum++
d.metrics.aggrGroups.Inc() // Insert the 1st alert in the group before starting the group's run()
// function, to make sure that when the run() will be executed the 1st
// alert is already there.
ag.insert(alert) // 处理pipeline并发送告警
go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool {
_, _, err := d.stage.Exec(ctx, d.logger, alerts...)
if err != nil {
lvl := level.Error(d.logger)
if ctx.Err() == context.Canceled {
// It is expected for the context to be canceled on
// configuration reload or shutdown. In this case, the
// message should only be logged at the debug level.
lvl = level.Debug(d.logger)
}
lvl.Log("msg", "Notify for alerts failed", "num_alerts", len(alerts), "err", err)
}
return err == nil
})
}

Pipeline的处理

在告警发送之前需要经过一系列的处理,这些处理称为Pipeline,由不同的Stage构成。

alertmanager的main()函数会初始化一个PipelineBuilderPipelineBuilder实现了Stage接口。

// A Stage processes alerts under the constraints of the given context.
type Stage interface {
Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error)
}

Stage翻译过来就是"阶段",对应Pipeline中的各个阶段,如GossipSettleWaitDedup等。

下面两个函数定义了如何初始化PipelineBuilder的各个Stage

func (pb *PipelineBuilder) New(
receivers map[string][]Integration,
wait func() time.Duration,
inhibitor *inhibit.Inhibitor,
silencer *silence.Silencer,
times map[string][]timeinterval.TimeInterval,
notificationLog NotificationLog,
peer Peer,
) RoutingStage {
rs := make(RoutingStage, len(receivers)) ms := NewGossipSettleStage(peer)
is := NewMuteStage(inhibitor)
tas := NewTimeActiveStage(times)
tms := NewTimeMuteStage(times)
ss := NewMuteStage(silencer) for name := range receivers {
st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics)
rs[name] = MultiStage{ms, is, tas, tms, ss, st}
}
return rs
}
// createReceiverStage creates a pipeline of stages for a receiver.
func createReceiverStage(
name string,
integrations []Integration,
wait func() time.Duration,
notificationLog NotificationLog,
metrics *Metrics,
) Stage {
var fs FanoutStage
for i := range integrations {
recv := &nflogpb.Receiver{
GroupName: name,
Integration: integrations[i].Name(),
Idx: uint32(integrations[i].Index()),
}
var s MultiStage
s = append(s, NewWaitStage(wait))
s = append(s, NewDedupStage(&integrations[i], notificationLog, recv))
s = append(s, NewRetryStage(integrations[i], name, metrics))
s = append(s, NewSetNotifiesStage(notificationLog, recv)) fs = append(fs, s)
}
return fs
}

根据alertmanager的架构图,其中最重要的Stage为::WaitStageDedupStageRetryStageSetNotifiesStage,即createReceiverStage函数中创建的几个Stage。

之前有讲过,processAlert函数会调用各个Stage的Exec()方法来处理告警,处理的告警内容为本示例中非恢复状态的告警。

WaitStage

顾名思义,WaitStage表示向其他实例发送Notification Log的时间间隔,只是单纯的时间等待。

// Exec implements the Stage interface.
func (ws *WaitStage) Exec(ctx context.Context, _ log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
select {
case <-time.After(ws.wait()):
case <-ctx.Done():
return ctx, nil, ctx.Err()
}
return ctx, alerts, nil
}

各个实例发送Notification Log的时长并不一样,它与p.Position()的返回值有关,timeout默认是15s。

// clusterWait returns a function that inspects the current peer state and returns
// a duration of one base timeout for each peer with a higher ID than ourselves.
func clusterWait(p *cluster.Peer, timeout time.Duration) func() time.Duration {
return func() time.Duration {
return time.Duration(p.Position()) * timeout
}
}
DedupStage和RetryStage

DedupStage目的就是根据告警的哈希值来判断本实例的告警是否已经被发送,如果已经被发送,则本实例不再继续发送。哈希算法如下,主要是对告警的标签进行哈希,后面再详细讲解该阶段。

func hashAlert(a *types.Alert) uint64 {
const sep = '\xff' hb := hashBuffers.Get().(*hashBuffer)
defer hashBuffers.Put(hb)
b := hb.buf[:0] names := make(model.LabelNames, 0, len(a.Labels)) for ln := range a.Labels {
names = append(names, ln)
}
sort.Sort(names) for _, ln := range names {
b = append(b, string(ln)...)
b = append(b, sep)
b = append(b, string(a.Labels[ln])...)
b = append(b, sep)
} hash := xxhash.Sum64(b) return hash
}

RetryStage的目的是将告警信息发送给各个用户配置的告警通道,如webhook、Email、wechat、slack等,如果设置了send_resolved: true,则还会发送告警恢复消息,并支持在连接异常的情况下使用指数退避的方式下进行重传。

SetNotifiesStage

该阶段就是使用Notification Log向其他节点发送告警通知的过程。这也是我们比较疑惑的阶段,既然同步了告警消息,为什么仍然会产生告警恢复?

下面是SetNotifiesStage的处理函数:

func (n SetNotifiesStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
gkey, ok := GroupKey(ctx)
if !ok {
return ctx, nil, errors.New("group key missing")
} firing, ok := FiringAlerts(ctx)
if !ok {
return ctx, nil, errors.New("firing alerts missing")
} resolved, ok := ResolvedAlerts(ctx)
if !ok {
return ctx, nil, errors.New("resolved alerts missing")
} // 通知其他实例
return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved)
}

首先通过FiringAlerts获取告警消息,通过ResolvedAlerts获取告警恢复消息,然后通过n.nflog.Log将这些消息发送给其他实例。可以看到FiringAlertsResolvedAlerts获取到的是[]uint64类型的数据,这些数据实际内容是什么?

func FiringAlerts(ctx context.Context) ([]uint64, bool) {
v, ok := ctx.Value(keyFiringAlerts).([]uint64)
return v, ok
} func ResolvedAlerts(ctx context.Context) ([]uint64, bool) {
v, ok := ctx.Value(keyResolvedAlerts).([]uint64)
return v, ok
}

答案是,SetNotifiesStage中用到的FiringAlertsResolvedAlerts是在DedupStage阶段生成的,因此SetNotifiesStage阶段发送给其他实例的信息实际是告警的哈希值

DedupStage处理如下:

func (n *DedupStage) Exec(ctx context.Context, _ log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
... firingSet := map[uint64]struct{}{}
resolvedSet := map[uint64]struct{}{}
firing := []uint64{}
resolved := []uint64{} var hash uint64
for _, a := range alerts {
hash = n.hash(a)
if a.Resolved() {
resolved = append(resolved, hash)
resolvedSet[hash] = struct{}{}
} else {
firing = append(firing, hash)
firingSet[hash] = struct{}{}
}
} //生成SetNotifiesStage使用的 FiringAlerts
ctx = WithFiringAlerts(ctx, firing)
//生成SetNotifiesStage使用的 ResolvedAlerts
ctx = WithResolvedAlerts(ctx, resolved) entries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv))
if err != nil && err != nflog.ErrNotFound {
return ctx, nil, err
} var entry *nflogpb.Entry
switch len(entries) {
case 0:
case 1:
entry = entries[0]
default:
return ctx, nil, errors.Errorf("unexpected entry result size %d", len(entries))
} if n.needsUpdate(entry, firingSet, resolvedSet, repeatInterval) {
return ctx, alerts, nil
}
return ctx, nil, nil
}

DedupStage阶段中会使用n.nflog.Query来接收其他实例SetNotifiesStage发送的信息,其返回的entries类型如下,从注释中可以看到FiringAlertsResolvedAlerts就是两个告警消息哈希数组:

type Entry struct {
// The key identifying the dispatching group.
GroupKey []byte `protobuf:"bytes,1,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"`
// The receiver that was notified.
Receiver *Receiver `protobuf:"bytes,2,opt,name=receiver,proto3" json:"receiver,omitempty"`
// Hash over the state of the group at notification time.
// Deprecated in favor of FiringAlerts field, but kept for compatibility.
GroupHash []byte `protobuf:"bytes,3,opt,name=group_hash,json=groupHash,proto3" json:"group_hash,omitempty"`
// Whether the notification was about a resolved alert.
// Deprecated in favor of ResolvedAlerts field, but kept for compatibility.
Resolved bool `protobuf:"varint,4,opt,name=resolved,proto3" json:"resolved,omitempty"`
// Timestamp of the succeeding notification.
Timestamp time.Time `protobuf:"bytes,5,opt,name=timestamp,proto3,stdtime" json:"timestamp"`
// FiringAlerts list of hashes of firing alerts at the last notification time.
FiringAlerts []uint64 `protobuf:"varint,6,rep,packed,name=firing_alerts,json=firingAlerts,proto3" json:"firing_alerts,omitempty"`
// ResolvedAlerts list of hashes of resolved alerts at the last notification time.
ResolvedAlerts []uint64 `protobuf:"varint,7,rep,packed,name=resolved_alerts,json=resolvedAlerts,proto3" json:"resolved_alerts,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

DedupStage阶段会使用和SetNotifiesStage阶段相同的哈希算法来计算本实例的告警的哈希值,然后与接收到的其他实例发送的告警哈希值进行对比,如果needsUpdate返回true,则会继续发送告警,如果返回false,则可以认为这部分告警已经被其他实例发送,本实例不再发送。

needsUpdate的函数如下,入参entry为接收到的其他实例发送的告警哈希值,firingresolved为本实例所拥有的告警哈希值,可以看到,如果要让本地不发送告警恢复,则满足如下条件之一即可:

  1. 本实例的firing哈希是entry.FiringAlerts的子集,即本实例的所有告警都已经被发送过
  2. 不启用发送告警恢复功能或本实例的resolved哈希是entry.ResolvedAlerts的子集(即本实例的所有告警恢复都已经被发送过)

也就是说,如果本实例的告警哈希与接收到的告警哈希存在交叉或完全不相同的情况时,则不会对告警消息和告警恢复消息产生抑制效果。

同时从上面也得出:alertmanager HA实例之间并不会同步具体的告警消息,它们只传递了告警的哈希值,且仅仅用于抑制告警和告警恢复。

func (n *DedupStage) needsUpdate(entry *nflogpb.Entry, firing, resolved map[uint64]struct{}, repeat time.Duration) bool {
// If we haven't notified about the alert group before, notify right away
// unless we only have resolved alerts.
if entry == nil {
return len(firing) > 0
} if !entry.IsFiringSubset(firing) {
return true
} // Notify about all alerts being resolved.
// This is done irrespective of the send_resolved flag to make sure that
// the firing alerts are cleared from the notification log.
if len(firing) == 0 {
// If the current alert group and last notification contain no firing
// alert, it means that some alerts have been fired and resolved during the
// last interval. In this case, there is no need to notify the receiver
// since it doesn't know about them.
return len(entry.FiringAlerts) > 0
} if n.rs.SendResolved() && !entry.IsResolvedSubset(resolved) {
return true
} // Nothing changed, only notify if the repeat interval has passed.
return entry.Timestamp.Before(n.now().Add(-repeat))
}

总结

至此,问题的根因也就清楚了。假设如下场景,alertmanager-1此时有2条firing的告警alert-1和alert-2,alertmanager-2有2条firing的告警alert-1和alert-3,由于使用了LB,导致发送到alertmanager-2的alert-1告警数目远少于alertmanager-1,且alertmanager-2的alert-1由于EndAt时间过老,即将产生告警恢复。而这种情况下alertmanager-2的firing告警哈希并不是alertmanager-1发送过来的告警哈希的子集,因此并不会产生抑制效果,之后便会导致alertmanager-2产生alert-1的告警恢复。

因此官方要求上游的告警必须能够发送到所有的alertmanager实例上。

alertmanager集群莫名发送resolve消息的问题探究的更多相关文章

  1. Nginx集群之WCF分布式消息队列

    目录 1       大概思路... 1 2       Nginx集群之WCF分布式消息队列... 1 3       MSMQ消息队列... 2 4       编写WCF服务.客户端程序... ...

  2. SpringCloud (十) Hystrix Dashboard单体监控、集群监控、与消息代理结合

    一.前言 Dashboard又称为仪表盘,是用来监控项目的执行情况的,本文旨在Dashboard的使用 分别为单体监控.集群监控.与消息代理结合. 代码请戳我的github 二.快速入门 新建一个Sp ...

  3. AlertManager集群搭建

    AlertManager集群搭建 一.AlertManager集群搭建 1.背景 2.机器 3.集群可用配置 4.alertmanager启动脚本 1.127.0.0.1:9083 机器启动脚本 2. ...

  4. Alertmanager 集群

    Alertmanager 集群搭建 环境准备:2台主机 (centos 7) 192.168.31.151 192.168.31.144 1.安装部署 192.168.31.151 cd /usr/l ...

  5. JAVAEE——宜立方商城08:Zookeeper+SolrCloud集群搭建、搜索功能切换到集群版、Activemq消息队列搭建与使用

    1. 学习计划 1.solr集群搭建 2.使用solrj管理solr集群 3.把搜索功能切换到集群版 4.添加商品同步索引库. a) Activemq b) 发送消息 c) 接收消息 2. 什么是So ...

  6. 使用rabbitmq实现集群im聊天服务器消息的路由

    这个地址图文会更清晰:https://www.jianshu.com/p/537e87c64ac7 单机系统的时候,客户端和连接都有同一台服务器管理.   image.png 在本地维护一份userI ...

  7. RabbitMQ集群下队列存放消息的问题

    RabbitMQ中队列有两种模式 1.默认 Default 2.镜像 Mirror [类似于mongoDB,从一直在通过主的操作日志来进行同步] *如果将队列定义为镜像模式,那么这个队列也将区分主从, ...

  8. tomcat源码阅读之集群

    一. 配置: 在tomcat目录下的conf/Server.xml配置文件中增加如下配置: <!-- Cluster(集群,族) 节点,如果你要配置tomcat集群,则需要使用此节点. clas ...

  9. Redis源码阅读(三)集群-连接初始化

    Redis源码阅读(三)集群-连接建立 对于并发请求很高的生产环境,单个Redis满足不了性能要求,通常都会配置Redis集群来提高服务性能.3.0之后的Redis支持了集群模式. Redis官方提供 ...

随机推荐

  1. Spring的事务控制-基于xml方式

    介绍:该程序模拟了转账操作,即Jone减少500元,tom增加500元 1.导入坐标 <dependency> <groupId>junit</groupId> & ...

  2. Spring相关的API-ApplicationContext

    1.ClassPathXmlApplicationContext 它是从类的根路径下加载配置文件推荐使用这种 public class UserController { public static v ...

  3. Java-GUI编程之绘图

    绘图 很多程序如各种小游戏都需要在窗口中绘制各种图形,除此之外,即使在开发JavaEE项目时,有时候也必须"动态"地向客户 端生成各种图形.图表,比如 图形验证码.统计图等,这都需 ...

  4. [翻译] 使用 TensorFlow 进行分布式训练

    本文以两篇官方文档为基础来学习TensorFlow如何进行分布式训练,借此进入Strategy世界.

  5. 记一次jenkins发送邮件报错 一直报错 Could not send email as a part of the post-build publishers问题

    写在前面 虽然Jenkins是开源.免费的,好处很多,但有些功能上的使用,我个人还是很不喜欢,感觉用起来特别麻烦.繁琐. 为什么? 就拿这个邮件配置来说吧,因重装系统,电脑需要配置很多东西,结果今天就 ...

  6. Sliding Window - 题解【单调队列】

    题面: An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving fr ...

  7. 9.1 Linux存储结构和文件系统

    1. 存储结构 Linux系统中的一切文件都是从"根"目录(/)开始的,并按照文件系统层次标准(FHS)采用倒树状结构来存放文件,以及定义了常见目录的用途. 目录名称 应放置文件的 ...

  8. 浅尝Spring注解开发_Bean生命周期及执行过程

    Spring注解开发 浅尝Spring注解开发,基于Spring 4.3.12 包含Bean生命周期.自定义初始化方法.Debug BeanPostProcessor执行过程及在Spring底层中的应 ...

  9. clion 预编译文件的查看

    看了一圈网上也没有我能一下就能看的懂的配置教程 我就手打一篇给在用clion的同学来参考一下 本文适用于g++编译 cmake Ninja生成器 clion 默认使用的是CMAKE来构建程序 生成器用 ...

  10. Linux 30岁,这些年经历了什么?

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 3月19日,Linux基金会在Twitter上发布推文宣布,其小企鹅的标志"Tux& ...