**Describe the solution you'd like**

We already can run clickhouse-server without configured ZooKeeper (the tables will start in read-only mode)
and with configured ZooKeeper, if it starts to be inaccessible at runtime (the tables will be read-only until the session is re-established).

Let's also make it possible to start up when ZooKeeper is unavailable.

**Additional context**

If that will be very hard to implement, we can consider other options:
- lazy attaching of the tables after sever start-up;
- a maintenance mode when the tables are not attached at start-up and require manual ATTACH queries.
zkutil::ZooKeeperPtr & zookeeper,
    const DataPartsVector & parts,
    const String & merged_name,
    const UUID & merged_part_uuid,
    const MergeTreeDataPartType & merged_part_type,
    bool deduplicate,
    const Names & deduplicate_by_columns,
    ReplicatedMergeTreeLogEntryData * out_log_entry,
    int32_t log_version,
    MergeType merge_type)
{
    std::vector<std::future<Coordination::ExistsResponse>> exists_futures;
    exists_futures.reserve(parts.size());
    for (const auto & part : parts)
        exists_futures.emplace_back(zookeeper->asyncExists(fs::path(replica_path) / "parts" / part->name));
    bool all_in_zk = true;
    for (size_t i = 0; i < parts.size(); ++i)
    {
        
        if (exists_futures[i].get().error == Coordination::Error::ZNONODE)
        {
            all_in_zk = false;
            const auto & part = parts[i];
            if (part->modification_time + MAX_AGE_OF_LOCAL_PART_THAT_WASNT_ADDED_TO_ZOOKEEPER < time(nullptr))
            {
                LOG_WARNING(log, "Part {} (that was selected for merge) with age {} seconds exists locally but not in ZooKeeper. Won't do merge with that part and will check it.", part->name, (time(nullptr) - part->modification_time));
                enqueuePartForCheck(part->name);
            }
        }