一、服务配置简介
ambari在安装服务(如:HDFS、SPARK、HIVE)时,会将各个服务的配置项内容(如hdfs-site.xml, core-site.xml)之间呈现在web管理界面上,用户可以任意修改服务的配置项。ambari会自动将这些服务配置分发给其集群内的所有主机,ambari-agent服务会将这些配置内容更新各个服务本身的配置文件,实现服务配置自动化,避免了在安装分布式服务(如HDFS、ZOOKEEPER、HBASE)时,需要使用scp命令手动拷贝配置文件到其他机器的问题。
二、源码解析
1. 服务配置项加载
在manager界面选择添加服务, 选中一个服务后, 进入配置服务步骤,页面先去请求了/api/v1/stacks/xxx/versions/xxxservices?StackServices/service_name.in(HDFS,HIVE,AMBARI_INFRA,SENTRY,YARN,MAPREDUCE2,ZOOKEEPER,LEAPSQL)&fields=configurations/*,configurations/dependencies/*,StackServices/config_types/*&_=1556434141234接口,服务端处理类是StackServiceResourceProvider,而在其子查询中,会去访问/services/xxx/configurations/xxx接口,该接口对应处理类是StackConfigurationResourceProvider类,获取服务配置关键代码如下(现在还没有在服务器上创建该service, 只能通过读取stack目录/LEAP/{stackVersion}/{serviceName}/configuration目录获取服务的所有配置):
public class StackConfigurationResourceProvider extends
ReadOnlyResourceProvider {
public Set<Resource> getResources(Request request, Predicate predicate) {
Set<StackConfigurationResponse> responses = getResources(new Command<Set<StackConfigurationResponse>>() {
@Override
public Set<StackConfigurationResponse> invoke() throws AmbariException {
return getManagementController().getStackConfigurations(requests);
}
});
}
}
getStackConfigurations()方法关键代码如下:
private Set<StackConfigurationResponse> getStackConfigurations(StackConfigurationRequest request) {
if (propertyName != null) {
properties = ambariMetaInfo.getPropertiesByName(stackName, stackVersion, serviceName, propertyName);
} else {
properties = ambariMetaInfo.getServiceProperties(stackName, stackVersion, serviceName);
}
for (PropertyInfo property: properties) {
response.add(property.convertToResponse(request.getLocale()));
}
}
AmbariMetaInfo类的getServiceProperties方法通过stackname, stackversion, servicename, 找到对应的ServiceInfo实例对象, 该对象里面含有该字段: private List<PropertyInfo> properties. 而该字段被赋值是在server初始启动时, ServiceModule类解析时, 调用populateConfigurationModules()类方法时. 赋值关键代码如下:
private void populateConfigurationModules() {
ConfigurationDirectory configDirectory = serviceDirectory.getConfigurationDirectory(
serviceInfo.getConfigDir(), AmbariMetaInfo.SERVICE_PROPERTIES_FOLDER_NAME);
if (configDirectory != null) {
for (ConfigurationModule config : configDirectory.getConfigurationModules()) {
ConfigurationInfo info = config.getModuleInfo();
serviceInfo.getProperties().addAll(info.getProperties());
serviceInfo.setTypeAttributes(config.getConfigType(), info.getAttributes());
configurationModules.put(config.getConfigType(), config);
}
}
在ConfigurationDirectory类初始化时, 会去读取configuration文件下的所有xml文件, 转化的关键代码如下:
private void parsePath() {
File[] configFiles = directory.listFiles(AmbariMetaInfo.FILENAME_FILTER);
if (configFiles != null) {
for (File configFile : configFiles) {
if (configFile.getName().endsWith(AmbariMetaInfo.SERVICE_CONFIG_FILE_NAME_POSTFIX)) {
String configType = ConfigHelper.fileNameToConfigType(configFile.getName());
ConfigurationXml config = null;
config = unmarshaller.unmarshal(ConfigurationXml.class, configFile);
ConfigurationInfo configInfo = new ConfigurationInfo(parseProperties(config,
configFile.getName()), parseAttributes(config));
ConfigurationModule module = new ConfigurationModule(configType, configInfo);
configurationModules.put(configType, module);
}
而ConfigurationXml类的结构代码如下:
@XmlRootElement(name="configuration")
public class ConfigurationXml implements Validable{
@XmlAnyAttribute
private Map<QName, String> attributes = new HashMap<QName, String>();
@XmlElement(name="property")
private List<PropertyInfo> properties = new ArrayList<PropertyInfo>();
@XmlTransient
private boolean valid = true;
}
其中attributes字段就是指configution标签本身的属性键值对, 例如: <configuration supports_final="true">.
这里要区分一个重要的概念就是properties是指configuration标签所有的子标签,而attributes是指configuration标签本身的键值对.
2. 保存服务配置到数据库
在确认配置选择下一步安装服务时, 会去调用该接口: /api/v1/clusters/{clusterName}. 而请求方式是PUT. 表单数据格式如下:
[{"Clusters":{"desired_config":[{"type":"leapid-log4j-env","tag":"version1583928110866","properties":{"leapid.log.MaxBackupIndex":"10","leapid.log.MaxFileSize":"100","leapid.log.console.Threshold":"ERROR","leapid.log.dir":"/usr/leapid-admin/logs","leapid.log.file.Threshold":"ERROR"}]
该表单的数据只会提交新加或者修改的配置, 以configType来计算, 例如修改了configType为hdfs-site的某一项配置, 将会提交这整个configType.
该请求在后端会被交给ClusterResourceProvider类处理, 该类会去调用AmbariManagerControllerImpl类的updateCluster()方法, 核心代码如下:
private synchronized RequestStatusResponse updateCluster(ClusterRequest request,
Map<String, String> requestProperties) {
// set or create configuration mapping (and optionally create the map of properties)
if (isConfigurationCreationNeeded) {
List<ConfigurationRequest> desiredConfigs = request.getDesiredConfig();
if (!desiredConfigs.isEmpty()) {
Set<Config> configs = new HashSet<Config>();
String note = null;
for (ConfigurationRequest cr : desiredConfigs) {
String configType = cr.getType();
if (null != cr.getProperties()) {
// !!! empty property sets are supported, and need to be able to use
// previously-defined configs (revert)
Map<String, Config> all = cluster.getConfigsByType(configType);
if (null == all || // none set
!all.containsKey(cr.getVersionTag()) || // tag not set
cr.getProperties().size() > 0) { // properties to set
// Ensure the user is allowed to update all properties
validateAuthorizationToUpdateServiceUsersAndGroups(cluster, cr);
cr.setClusterName(cluster.getClusterName());
configurationResponses.add(createConfiguration(cr));
}
}
note = cr.getServiceConfigVersionNote();
configs.add(cluster.getConfig(configType, cr.getVersionTag()));
}
if (!configs.isEmpty()) {
String authName = getAuthName();
serviceConfigVersionResponse = cluster.addDesiredConfig(authName,
configs, note);
}
}
}
在这里面最终调用了createrConfiguration方法, 关键代码如下:
public synchronized ConfigurationResponse createConfiguration(
ConfigurationRequest request) {
Config config = createConfig(cluster, request.getType(), requestProperties,
request.getVersionTag(), propertiesAttributes);
return new ConfigurationResponse(cluster.getClusterName(), config);
}
public Config createConfig(Cluster cluster, String type, Map<String, String> properties,
String versionTag, Map<String, Map<String, String>> propertiesAttributes) {
Config config = configFactory.createNew(cluster, type,
properties, propertiesAttributes);
if (!StringUtils.isEmpty(versionTag)) {
config.setTag(versionTag);
}
config.persist();
cluster.addConfig(config);
return config;
}
在createConfig()方法里, 会生成ConfigImpl对象实例, 该对象的persist()方法会生成ClusterConfigEntity实体类, 并调用相应的dao将数据存入数据库, 同时将该config对象实例加入cluster对象的allconfigs属性集合中.
在所有Config对象创建完成后, 上面的updateCluster方法最后调用addDesiredConfig()方法, 关键代码如下:
public ServiceConfigVersionResponse addDesiredConfig(String user, Set<Config> configs, String serviceConfigVersionNote) {
ServiceConfigVersionResponse serviceConfigVersionResponse = applyConfigs(
configs, user, serviceConfigVersionNote);
configHelper.invalidateStaleConfigsCache(getDesiredConfigs());
return serviceConfigVersionResponse;
}
ServiceConfigVersionResponse applyConfigs(Set<Config> configs, String user, String serviceConfigVersionNote) {
if (serviceName == null) {
ArrayList<String> configTypes = new ArrayList<>();
for (Config config: configs) {
configTypes.add(config.getType());
}
LOG.error("No service found for config types '{}', service config version not created", configTypes);
return null;
} else {
return createServiceConfigVersion(serviceName, user, serviceConfigVersionNote);
}
}
最终调用的createServiceConfigVersion()方法会去创建ServiceConfigEntity对象, 并调用相应的dao将数据存入数据库中.
三、 Manager界面出现重启标识的原因
1. 当在界面上添加服务时, 会创建ServiceHostComponentImpl对象. server向agent端发送start命令, 在命令中会带上所有config的name以及它们所对应的tag.
2. agent端在收到executecommand后, 会将这些config tag记录存入/var/lib/ambari-agent/data/cluster_config.json文件中
3. 在向server回执的心跳数据中, 会带上这些config tag, server端收到回复后, 回去调用ServiceHostComponentImple的updateActualConfig()方法更新actualConfig值, 同时设置restartServiceRequired为true.