侧边栏壁纸
博主头像
luoxx博主等级

只要思想不滑坡,办法总比困难多

  • 累计撰写 58 篇文章
  • 累计创建 64 个标签
  • 累计收到 1,204 条评论

目 录CONTENT

文章目录

halo集群部署改造方案

luoxx
2022-03-15 / 7 评论 / 3 点赞 / 6,055 阅读 / 2,537 字
温馨提示:
本文最后更新于 2023-04-11,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除,邮箱地址:luoxmc@vip.qq.com

halo是基于java开发的一款现代化的开源博客/CMS系统,各方面都做得非常不错,但是截止1.5.0-beta.1版本为止,还没有做对分布式部署相关的支持,集群部署可能会频繁的需要重新登陆,所以我这边研究了一下halo的代码,做了一些改造以支持分布式部署。

前言

怎么部署集群halo,这个完全是由你自己的负载均衡方式来决定的。

如果你是用的是nginx之类的软件负载均衡方式,那么很简单,并不需要做什么代码改造,只需要将nginx中halo服务配置为ip_hash模式即可,配置好之后,所有请求都有nginx分发,nginx服务会自动负载均衡使同一个ip的请求每次都指向同一个机器的服务,这样就不存在session会话不一致的问题了。

但是这个方式有个缺点,你的nginx服务必须在负载均衡链路的最前端(也就是第一层负载均衡),相当于所有请求、流量都必须从nginx所在机器进出。所以nginx所在机器的带宽即你这个网站的能容纳的带宽上限,不能把所有请求比较均匀的分布在多台机器上,那我们部署分布式halo系统还有啥意义。

所以我这边采用的是dns负载均衡的方式,由域名的dns解析服务商来给我们做负载均衡,用户访问域名的时候由dns解析服务轮询分配不同机器的ip给客户,这样我们网站能容纳的带宽就是两台机器的带宽相加了(严格来说不是,但是起码能同时接受更多个用户同时访问了)

分布式部署

  1. 第一台机器的部署很简单,官网有非常详细的教程,按照步骤一步一步来就ok了。地址:在 Linux 环境部署halo ,部署完成之后启动服务。

  2. 第二台机器的部署就稍微复杂一点点。先按照官网教程走完第4步 创建 工作目录 ,之后不要急着去创建配置文件,我们要保证两台机器的数据一致,所以直接把第一台机器的 工作目录 挂载到第二台机器的 工作目录 。 这样所有的配置文件、上传文件、主题文件、数据库文件(使用h2数据库的情况下)就全部都同步了起来,具体的挂载办法请看我之前的文章 mount挂载远程目录(centos7.6)ps: halo的工作目录为 ~/.halo

  3. mount成功之后就可以启动服务了,如果还有第三台第四台机器的话,重复上面的第2步即可。 ps: 请使用内网ip来操作mount,不然延迟会很大。

  4. 接下来就可以配置dns解析了,只需要把不同机器的ip都配置到A记录即可,不同dns解析提供商的配置界面不一样,比如腾讯云的dns配置如下图

image

  1. dns生效之后通过域名就能访问到不同机器的halo服务了

分布式改造思路

虽然负载均衡已经实现了,但是这种方式在后台操作的时候经常会报未登录,这是因为访问的域名虽然没变,但是访问的机器已经变了,本地存的token新的机器并不认识,所以经常会报未登录。

思路一

最开始我想着这个应该是集群session共享的问题,只要把多台机器的session想办法共享一下就ok了,在github的issues里面也有一个这样的issue 多机部署后台管理登录Session混乱 #1505 也是描述了类似的问题。不过我下载代码后发现halo的用户验证相关逻辑并不是依赖于session操作的,而是单独保存了一个token来做校验,所以session共享这个方案就pass了

思路二

halo的官网文档上有写,halo支持两种存储策略

memory 将数据缓存至内存,重启服务缓存将清空。
level 将数据缓存至本地,重启服务不会清空缓存。

默认的方式是保存在内存中,也可以切换成本地文件存储,所以这时候我又想着既然是文件存储,那直接mount共享不就ok了么。看代码之后发现本地文件就是保存在 ~/.halo 目录的,好家伙,都不需要额外mount了,直接改下cache方式就完事了。

然而现实并没有那么美好,本地存储使用的是leveldb,而leveldb操作的文件有文件锁,两个机器共享直接报错,无法读取。

而这个leveldb我以前也没用过,也不太熟,直接改原本地存储的逻辑也不太合适,所以这个方案也pass了。

思路三

既然本地存储不行,那就直接用第三方存储呗,所以我马上就敲定了使用redis来做缓存的方案,多台实例访问同一个redis,缓存数据就实现了共享,就再也不需要重新登陆了。

分布式改造实施

研究源码发现,缓存这一块的封装、继承都做的很好,不同的存储方案基本上只需要新建不同的实现类即可

image-1647327454329

如图黄框内是原有的内存、文件存储两种方式的实现类,蓝框内是我新增的redis缓存的实现类

改造的逻辑就是新增一种存储方式(redis),做到可配置,不使用此种方式时可以不配redis。

核心代码如下:


package run.halo.app.cache;

import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;

/**
 * redis cache store.
 *
 * @author luoxx
 */
@Slf4j
public class RedisCacheStore extends AbstractStringCacheStore {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String REDIS_PREFIX = "halo.redis.";

    @Override
    @NonNull
    Optional<CacheWrapper<String>> getInternal(@NonNull String key) {
        Assert.hasText(REDIS_PREFIX + key, "Cache key must not be blank");
        String value = redisTemplate.opsForValue().get(REDIS_PREFIX + key);
        CacheWrapper<String> cacheStore = new CacheWrapper<>();
        cacheStore.setData(value);
        return Optional.of(cacheStore);
    }

    @Override
    void putInternal(@NonNull String key, @NonNull CacheWrapper<String> cacheWrapper) {
        Assert.hasText(key, "Cache key must not be blank");
        Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
        if (cacheWrapper.getExpireAt() != null) {
            long expire = cacheWrapper.getExpireAt().getTime() - System.currentTimeMillis();
            redisTemplate.opsForValue().set(
                REDIS_PREFIX + key, cacheWrapper.getData(), expire, TimeUnit.MILLISECONDS);
        } else {
            redisTemplate.opsForValue().set(REDIS_PREFIX + key, cacheWrapper.getData());
        }

        log.debug("Put [{}] cache : [{}]", key, cacheWrapper);
    }

    @Override
    Boolean putInternalIfAbsent(@NonNull String key, @NonNull CacheWrapper<String> cacheWrapper) {
        Assert.hasText(key, "Cache key must not be blank");
        Assert.notNull(cacheWrapper, "Cache wrapper must not be null");

        log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper);

        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
            log.warn("Failed to put the cache, the key: [{}] has been present already", key);
            return false;
        }

        putInternal(key, cacheWrapper);
        log.debug("Put successfully");
        return true;

    }

    @Override
    public Optional<String> get(String key) {
        Assert.notNull(key, "Cache key must not be blank");

        return getInternal(key).map(CacheWrapper::getData);
    }

    @Override
    public void delete(@NonNull String key) {
        Assert.hasText(key, "Cache key must not be blank");

        if (Boolean.TRUE.equals(redisTemplate.hasKey(REDIS_PREFIX + key))) {
            redisTemplate.delete(REDIS_PREFIX + key);
            log.debug("Removed key: [{}]", key);
        }
    }

    @Override
    public LinkedHashMap<String, String> toMap() {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        Set<String> keys = redisTemplate.keys(REDIS_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            for (String key : keys) {
                map.put(key, redisTemplate.opsForValue().get(key));
            }
        }
        return map;
    }

    @PreDestroy
    public void preDestroy() {
        //do nothing
    }

}


实测,经过改造后,确实不会再弹未登录提示了,博客各项功能暂时也正常。

改动的所有代码我都提交到了github,可以直接去github查看。 地址: 提交记录

改动也发了pr到halo的主分支,不过不确定能不能通过,要是能通过的话,以后的官方版本就能支持redis缓存了。没通过的话也没关系,我这边会把打好的包发布在这里,供需要的朋友下载使用。教程如下:

(更新:pr已通过,已经在1.5.0正式版本中集成了redis配置相关代码,直接下载官方包即可)

使用教程

  • 下载地址:
    halo-1.5.0-luoxx.jar

  • 如何安装redis(安装一台即可,缓存很少,没必要redis也搞集群了)


#安装
yum install redis
#启动
service redis start

修改redis密码:只需修改 /etc/redis.conf 文件,将 #requirepass foobared 修改为 requirepass 你的密码

放开访问权限:在/etc/redis.conf 文件中找到 bind 127.0.0.1 这一行,并注释或删除

修改之后执行service redis restart重启redis

  • 如何更新halo
    请参照官网的教程 版本升级

  • 配置修改
    修改 ~/.halo 目录下的 application.yaml 配置文件, 缓存方式修改为redis (修改前请先备份)


cache: redis

新增redis数据库相关配置,注意层级是在spring模块下,若redis未设置密码则删除password项


redis:
    port: 6379
    database: 0
    host: 127.0.0.1
    password: 123456

配置完成,并且更新jar包后,分别重启多台机器的halo服务即可

完整的示例配置如下


server:
  port: 8090

  # Response data gzip.
  compression:
    enabled: false

spring:
  datasource:

    # MySQL database configuration.
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/halodb?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456

  redis:
    port: 6379
    database: 0
    host: 127.0.0.1
    password: 123456

halo:

  # Your admin client path is https://your-domain/{admin-path}
  admin-path: admin

  # memory or level or redis
  cache: redis

3

评论区